mirror of
https://github.com/UiO-Gaming/ShiteMusicBot.git
synced 2026-11-05 13:56:51 +01:00
Merge branch 'dev' into feature/improved_readme
This commit is contained in:
33 files changed
+858
-499
No files matched your search
@@ -2,7 +2,7 @@ name: Test And Build BOT
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
flake8:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CheckFiles: "bot.py musicbot/"
|
||||
@@ -24,14 +24,27 @@ jobs:
|
||||
pip install isort
|
||||
isort --check --sp setup.cfg ${CheckFiles}
|
||||
|
||||
- name: Run tests
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@v4.2.0
|
||||
with:
|
||||
python-version: 3.10.7
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install pytest
|
||||
pip install -r requirements.txt
|
||||
|
||||
- name: Run pytest
|
||||
run: |
|
||||
pytest
|
||||
|
||||
build-and-push:
|
||||
needs: flake8
|
||||
needs: [lint, test]
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name != 'pull_request' && contains(github.ref, 'actions') != 'true'
|
||||
env:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Discord Packages
|
||||
import discord
|
||||
import lavalink
|
||||
from discord.ext import commands
|
||||
from discord.flags import MemberCacheFlags
|
||||
|
||||
@@ -8,6 +9,7 @@ import os
|
||||
import time
|
||||
import traceback
|
||||
from argparse import ArgumentParser, RawTextHelpFormatter
|
||||
from typing import Optional
|
||||
|
||||
import aiohttp
|
||||
import yaml
|
||||
@@ -35,7 +37,7 @@ def _get_prefix(bot, message):
|
||||
return commands.when_mentioned_or(*prefixes)(bot, message)
|
||||
|
||||
|
||||
class Bot(commands.Bot):
|
||||
class MusicBot(commands.Bot):
|
||||
def __init__(self, datadir, debug: bool = False):
|
||||
intents = discord.Intents.all()
|
||||
super().__init__(command_prefix=_get_prefix,
|
||||
@@ -55,6 +57,7 @@ class Bot(commands.Bot):
|
||||
self.main_logger = logger
|
||||
self.logger = self.main_logger.bot_logger.getChild("Bot")
|
||||
self.logger.debug("Debug: %s" % debug)
|
||||
self.lavalink: Optional[lavalink.Client] = None
|
||||
|
||||
async def on_message(self, message):
|
||||
if message.author.bot:
|
||||
@@ -106,11 +109,11 @@ class Bot(commands.Bot):
|
||||
except Exception as e:
|
||||
tb = e.__traceback__
|
||||
traceback.print_tb(tb)
|
||||
self.logger.error(e)
|
||||
self.logger.exception(e)
|
||||
|
||||
|
||||
def run_bot(datadir, debug: bool = False):
|
||||
bot = Bot(datadir, debug=debug)
|
||||
bot = MusicBot(datadir, debug=debug)
|
||||
bot.run()
|
||||
|
||||
|
||||
|
||||
@@ -50,11 +50,11 @@ move:
|
||||
description: 'Moves a track in your queue.'
|
||||
remove:
|
||||
aliases: [rem]
|
||||
args: '<pos>'
|
||||
args: '<member>'
|
||||
description: 'Removes a song from your queue.'
|
||||
DJremove:
|
||||
aliases: [DJremove, djremove]
|
||||
args: '<pos>'
|
||||
args: '<member>'
|
||||
description: 'Removes a song from the global queue.'
|
||||
removeuser:
|
||||
aliases: [removeuser]
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
bot:
|
||||
name: What?
|
||||
infotext: Music bot written for use on /r/Norge's Discord Servers. It's open source!
|
||||
infotext:
|
||||
Music bot written for use on /r/Norge's Discord Servers. It's open source!
|
||||
You can find it [HERE]({global.vars.repo})
|
||||
spectext: |-
|
||||
**Python:** [{_python_v}](https://www.python.org/)
|
||||
**Discord.py:** [{_discord_v}](https://github.com/Rapptz/discord.py/tree/rewrite)
|
||||
**Lavalink:** [{_lavalink_v}](https://github.com/Devoxin/Lavalink.py)
|
||||
**ShiteMusicBot:** {_bot_v}
|
||||
**Discord.py:** [{_discord_v}](https://github.com/Rapptz/discord.py/releases/tag/v{_discord_v})
|
||||
**Lavalink:** [{_lavalink_v}](https://github.com/Devoxin/Lavalink.py/releases/tag/{_lavalink_v})
|
||||
**ShiteMusicBot:** [{_bot_v}](https://github.com/r-Norge/ShiteMusicBot)
|
||||
stattext: |-
|
||||
**Guilds:** {_guilds}
|
||||
**Users:** {_members}
|
||||
|
||||
@@ -1 +1 @@
|
||||
repo: "https://github.com/Ev-1/ShiteMusicBot"
|
||||
repo: "https://github.com/r-Norge/ShiteMusicBot"
|
||||
@@ -54,7 +54,7 @@ remove:
|
||||
description: 'Fjerner en sang fra personlige køen din.'
|
||||
DJremove:
|
||||
aliases: [DJfjern, djfjern]
|
||||
args: '<posisjon>'
|
||||
args: '<servermedlem>'
|
||||
description: 'Fjerner en sang fra køen.'
|
||||
removeuser:
|
||||
aliases: [fjernbruker]
|
||||
@@ -78,6 +78,10 @@ boost:
|
||||
aliases: [bassforsterker, forsterkbass]
|
||||
args: ''
|
||||
description: 'Skrur av/på bassforsterkeren'
|
||||
nightcore:
|
||||
aliases: [natt, nattkjernemodus, natteravn]
|
||||
args: <True/False>
|
||||
description: 'Skrur av/på nattkjernemodus'
|
||||
history:
|
||||
aliases: [historikk, avspillingshistorikk, h]
|
||||
description: 'Viser en liste over de 10 siste avspilte sangene.'
|
||||
@@ -88,6 +92,12 @@ lyrics:
|
||||
scrub:
|
||||
aliases: [kontroller]
|
||||
description: 'Gir deg en kontroller som du kan styre musikken med.'
|
||||
reconnect:
|
||||
aliases: [rc]
|
||||
description: 'Kobler båtten av, så på'
|
||||
forcedisconnect:
|
||||
aliases: [fd]
|
||||
description: 'Tvinger båtten til å koble av'
|
||||
forceplay:
|
||||
aliases: [tvingspill, ts]
|
||||
args: '<sang>' # This is just a cosmetic representation of the arguments.
|
||||
@@ -115,7 +125,7 @@ uptime:
|
||||
description: 'Viser hvor lenge båtten har kjørt.'
|
||||
ping:
|
||||
aliases: [ping]
|
||||
description: 'Sjekker responstiden til båtten.'
|
||||
description: 'Sjekker responstiden til båtten.'
|
||||
guilds:
|
||||
aliases: [tenarar]
|
||||
description: 'Sender en liste over tenarane som båtten er medlem av'
|
||||
@@ -126,6 +136,24 @@ reloadalias:
|
||||
aliases: [reloadalias]
|
||||
description: 'Laster inn aliaser og hjelp på nytt.'
|
||||
|
||||
# NodeManager
|
||||
node:
|
||||
aliases: [node]
|
||||
description: 'Håndterer noder for serveren.'
|
||||
sub_commands:
|
||||
add:
|
||||
aliases: [ny]
|
||||
description: 'Legger til en ny node.'
|
||||
list:
|
||||
aliases: [liste, noder]
|
||||
description: 'Viser en liste over konfigurerte noder.'
|
||||
remove:
|
||||
aliases: [fjern]
|
||||
description: 'Fjerner noder.'
|
||||
reload_file:
|
||||
aliases: [last_fil]
|
||||
description: 'Leser innstillingsfilen og legger til nye noder.'
|
||||
|
||||
# Settings
|
||||
settings:
|
||||
aliases: [innstillinger, oppsett]
|
||||
@@ -200,4 +228,5 @@ cogmanager:
|
||||
description: 'Laser inn alle cogs på nytt.'
|
||||
shutdown:
|
||||
aliases: [shutdown]
|
||||
description: 'Stopper båtten.'
|
||||
description: 'Stopper båtten.'
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
bot:
|
||||
name: hva?
|
||||
infotext: Musikkbot skrevet for bruk på /r/Norge sine Discord Servere. Kildekoden
|
||||
infotext:
|
||||
Musikkbot skrevet for bruk på /r/Norge sine Discord Servere. Kildekoden
|
||||
er åpen! Den kan du finne [HER]({global.vars.repo})
|
||||
spectext: |-
|
||||
**Python:** [{_python_v}](https://www.python.org/)
|
||||
**Discord.py:** [{_discord_v}](https://github.com/Rapptz/discord.py/tree/rewrite)
|
||||
**Lavalink:** [{_lavalink_v}](https://github.com/Devoxin/Lavalink.py)
|
||||
**ShiteMusicBot:** {_bot_v}
|
||||
**Discord.py:** [{_discord_v}](https://github.com/Rapptz/discord.py/releases/tag/v{_discord_v})
|
||||
**Lavalink:** [{_lavalink_v}](https://github.com/Devoxin/Lavalink.py/releases/tag/{_lavalink_v})
|
||||
**ShiteMusicBot:** [{_bot_v}](https://github.com/r-Norge/ShiteMusicBot)
|
||||
stattext: |-
|
||||
**Tenarar:** {_guilds}
|
||||
**Brukere:** {_members}
|
||||
|
||||
@@ -55,6 +55,9 @@ volume:
|
||||
boost:
|
||||
'on': Bassforsterkeren er på
|
||||
'off': Bassforsterkeren er av
|
||||
nightcore:
|
||||
'on': Nattkjernemodus er på
|
||||
'off': Nattkjernemodus er av
|
||||
scrub:
|
||||
add: Legger til kontrollere
|
||||
controls: "{music_controls}"
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
bot:
|
||||
name: Kva?
|
||||
infotext: Musikkbot skreve til bruk på /r/Norge sine Discord Serverar. Kjeldekoda
|
||||
infotext:
|
||||
Musikkbot skreve til bruk på /r/Norge sine Discord Serverar. Kjeldekoda
|
||||
er ope! Dei kan du finna [HER]({global.vars.repo})
|
||||
spectext: |-
|
||||
**Python:** [{_python_v}](https://www.python.org/)
|
||||
**Discord.py:** [{_discord_v}](https://github.com/Rapptz/discord.py/tree/rewrite)
|
||||
**Lavalink:** [{_lavalink_v}](https://github.com/Devoxin/Lavalink.py)
|
||||
**Discord.py:** [{_discord_v}](https://github.com/Rapptz/discord.py/releases/tag/v{_discord_v})
|
||||
**Lavalink:** [{_lavalink_v}](https://github.com/Devoxin/Lavalink.py/releases/tag/{_lavalink_v})
|
||||
**ShiteMusicBot:** [{_bot_v}](https://github.com/r-Norge/ShiteMusicBot)
|
||||
stattext: |-
|
||||
**Tenarar:** {_guilds}
|
||||
**Brukarar:** {_members}
|
||||
|
||||
@@ -54,7 +54,7 @@ remove:
|
||||
description: 'Wemoves a sonyg fwom youw queue.'
|
||||
DJremove:
|
||||
aliases: [DJwemove, djwemove]
|
||||
args: '<pos>'
|
||||
args: '<member>'
|
||||
description: 'Wemoves a sonyg fwom the gwobaw queue.'
|
||||
removeuser:
|
||||
aliases: [wemoveusew]
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
bot:
|
||||
name: What?
|
||||
infotext: Music bot wwitteny fow use ony /r/Norges Discord Sewvews. Its openy souwce!
|
||||
infotext:
|
||||
Music bot wwitteny fow use ony /r/Norges Discord Sewvews. Its openy souwce!
|
||||
You cany finyd it [HEWE]({global.vars.repo})
|
||||
spectext: |-
|
||||
**Python:** [{_python_v}](https://www.python.org/)
|
||||
**Discord.py:** [{_discord_v}](https://github.com/Rapptz/discord.py/tree/rewrite)
|
||||
**Lavalink:** [{_lavalink_v}](https://github.com/Devoxin/Lavalink.py)
|
||||
**Discord.py:** [{_discord_v}](https://github.com/Rapptz/discord.py/releases/tag/v{_discord_v})
|
||||
**Lavalink:** [{_lavalink_v}](https://github.com/Devoxin/Lavalink.py/releases/tag/{_lavalink_v})
|
||||
**ShiteMusicBot:** [{_bot_v}](https://github.com/r-Norge/ShiteMusicBot)
|
||||
stattext: |-
|
||||
**Guiwds:** {_guilds}
|
||||
**Usews:** {_members}
|
||||
|
||||
@@ -3,13 +3,15 @@ from discord.ext import commands
|
||||
|
||||
import traceback
|
||||
|
||||
from bot import MusicBot
|
||||
|
||||
from ..utils.userinteraction import ClearOn, Scroller
|
||||
from .helpformatter import commandhelper
|
||||
|
||||
|
||||
class CogManager(commands.Cog):
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
def __init__(self, bot: MusicBot):
|
||||
self.bot: MusicBot = bot
|
||||
self.settings = self.bot.settings
|
||||
|
||||
@commands.group(name='cogmanager', hidden=True)
|
||||
@@ -72,7 +74,8 @@ class CogManager(commands.Cog):
|
||||
@commands.is_owner()
|
||||
async def _shutdown(self, ctx):
|
||||
"""Logs out and stops."""
|
||||
self.bot.lavalink.player_manager.players.clear()
|
||||
if lavalink := self.bot.lavalink:
|
||||
lavalink.player_manager.players.clear()
|
||||
await self.bot.close()
|
||||
|
||||
|
||||
|
||||
+53
-44
@@ -2,16 +2,18 @@
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
import traceback
|
||||
import sys
|
||||
|
||||
from bot import MusicBot
|
||||
|
||||
from ..utils.userinteraction import ClearOn, Scroller
|
||||
from .helpformatter import commandhelper
|
||||
from .music.music_errors import WrongTextChannelError, WrongVoiceChannelError
|
||||
from .music import music_errors
|
||||
|
||||
|
||||
class Errors(commands.Cog):
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
def __init__(self, bot: MusicBot):
|
||||
self.bot: MusicBot = bot
|
||||
self.logger = self.bot.main_logger.bot_logger.getChild("Errors")
|
||||
|
||||
async def base_msg(self, ctx, state: int = 0xFCBA03):
|
||||
@@ -42,53 +44,61 @@ class Errors(commands.Cog):
|
||||
embed = ctx.localizer.format_embed(embed)
|
||||
return await ctx.send(embed=embed)
|
||||
|
||||
# User input errors
|
||||
if isinstance(err, commands.UserInputError):
|
||||
if self.bot.debug:
|
||||
self.logger.debug("User input error")
|
||||
self.logger.debug(err)
|
||||
return
|
||||
|
||||
# Music related errors
|
||||
if isinstance(err, commands.CommandInvokeError):
|
||||
if (err.original == 'Join a voicechannel first.'):
|
||||
if isinstance(err, music_errors.MusicError):
|
||||
if isinstance(err, music_errors.UserNotConnectedError):
|
||||
return await send_error_embed('{errors.join_voice_first}')
|
||||
|
||||
elif (err.original == 'Not connected.'):
|
||||
elif isinstance(err, music_errors.BotNotConnectedError):
|
||||
return await send_error_embed('{errors.not_connected}')
|
||||
|
||||
elif (err.original == 'I need the `CONNECT` and `SPEAK` permissions.'):
|
||||
elif isinstance(err, music_errors.MissingPermissionsError):
|
||||
return await send_error_embed('{errors.need_permission}')
|
||||
|
||||
elif (err.original == 'Not playing'):
|
||||
return await send_error_embed('{not_playing}')
|
||||
elif isinstance(err, music_errors.PlayBackStatusError):
|
||||
if isinstance(err, music_errors.RequirePlayingError):
|
||||
return await send_error_embed('{not_playing}')
|
||||
if isinstance(err, music_errors.RequireListeningError):
|
||||
return await send_error_embed('{have_to_listen}')
|
||||
|
||||
elif (err.original == 'Not listening'):
|
||||
return await send_error_embed('{have_to_listen}')
|
||||
elif isinstance(err, music_errors.ChannelError):
|
||||
if isinstance(err, music_errors.VoiceChannelFullError):
|
||||
return await send_error_embed('{errors.full_channel}')
|
||||
|
||||
elif (err.original == 'The channel is currently full'):
|
||||
return await send_error_embed('{errors.full_channel}')
|
||||
elif isinstance(err, music_errors.UserInDifferentVoiceChannelError):
|
||||
return await send_error_embed(err.channels[0].name, title='{errors.my_channel}')
|
||||
|
||||
if isinstance(err, WrongVoiceChannelError):
|
||||
if (err.original.startswith("You need to be in my voice channel")):
|
||||
return await send_error_embed(err.channels[0].name, title='{errors.my_channel}')
|
||||
elif isinstance(err, music_errors.ConfiguredChannelsError):
|
||||
if isinstance(err, music_errors.WrongVoiceChannelError):
|
||||
changed = False
|
||||
response = ctx.localizer.format_str('{settings_check.voicechannel}')
|
||||
for channel_id in err.channels:
|
||||
channel = ctx.guild.get_channel(channel_id)
|
||||
if channel is not None:
|
||||
response += f'{channel.name}, '
|
||||
changed = True
|
||||
if changed:
|
||||
return await send_error_embed(response[:-2], title='{errors.right_channel}')
|
||||
else:
|
||||
return await send_error_embed('{errors.right_channel}')
|
||||
|
||||
elif (err.original.startswith('You need to be in the right voice channel')):
|
||||
changed = False
|
||||
response = ctx.localizer.format_str('{settings_check.voicechannel}')
|
||||
for channel_id in err.channels:
|
||||
channel = ctx.guild.get_channel(channel_id)
|
||||
if channel is not None:
|
||||
response += f'{channel.name}, '
|
||||
changed = True
|
||||
if changed:
|
||||
return await send_error_embed(response[:-2], title='{errors.right_channel}')
|
||||
else:
|
||||
return await send_error_embed('{errors.right_channel}')
|
||||
|
||||
if isinstance(err, WrongTextChannelError):
|
||||
try:
|
||||
await ctx.message.delete()
|
||||
except Exception as e:
|
||||
self.logger.debug("Error deleting message: %s\nTraceback: %s" % (e, err))
|
||||
response = ctx.localizer.format_str('{settings_check.textchannel}')
|
||||
for channel_id in err.channels:
|
||||
response += f'<#{channel_id}>, '
|
||||
msg = await send_error_embed(response[:-2], title='{errors.right_channel}')
|
||||
return await msg.delete(delay=5)
|
||||
if isinstance(err, music_errors.WrongTextChannelError):
|
||||
try:
|
||||
await ctx.message.delete()
|
||||
except Exception as e:
|
||||
self.logger.debug("Error deleting message: %s\nTraceback: %s" % (e, err))
|
||||
response = ctx.localizer.format_str('{settings_check.textchannel}')
|
||||
for channel_id in err.channels:
|
||||
response += f'<#{channel_id}>, '
|
||||
msg = await send_error_embed(response[:-2], title='{errors.right_channel}')
|
||||
return await msg.delete(delay=5)
|
||||
|
||||
if isinstance(err, commands.CheckFailure):
|
||||
pass
|
||||
@@ -103,16 +113,15 @@ class Errors(commands.Cog):
|
||||
else:
|
||||
# Log all exceptions if the bot is in debug mode
|
||||
if self.bot.debug:
|
||||
tb = err.__traceback__
|
||||
traceback.print_tb(tb)
|
||||
self.logger.debug("Error running command: %s\nTraceback: %s" % (ctx.command, err))
|
||||
self.logger.error("Got error in command %s" % ctx.command)
|
||||
self.logger.exception(err.with_traceback(sys.exc_info()[2]))
|
||||
|
||||
else:
|
||||
to_log = (RuntimeError, commands.CheckFailure, commands.CommandInvokeError,
|
||||
commands.NoPrivateMessage)
|
||||
|
||||
if isinstance(err, to_log):
|
||||
self.logger.debug("Error running command: %s\nTraceback: %s" % (ctx.command, err))
|
||||
self.logger.info("Error running command: %s\nTraceback: %s" % (ctx.command, err))
|
||||
|
||||
|
||||
async def setup(bot):
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
# Discord Packages
|
||||
from discord.ext import commands
|
||||
|
||||
from bot import MusicBot
|
||||
|
||||
from ...utils.userinteraction.scroller import ClearOn, Scroller
|
||||
from .helpformatter import coghelper, commandhelper, helper, prefix_cleaner
|
||||
|
||||
|
||||
class Help(commands.Cog):
|
||||
"""Help command"""
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
def __init__(self, bot: MusicBot):
|
||||
self.bot: MusicBot = bot
|
||||
|
||||
@commands.command(hidden=True)
|
||||
async def help(self, ctx): # Takes no args because reasons(using the view directly)
|
||||
|
||||
@@ -6,12 +6,14 @@ from lavalink import __version__ as LavalinkVersion
|
||||
import platform
|
||||
import time
|
||||
|
||||
from bot import MusicBot
|
||||
|
||||
from ..utils import bot_version
|
||||
|
||||
|
||||
class Misc(commands.Cog):
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
def __init__(self, bot: MusicBot):
|
||||
self.bot: MusicBot = bot
|
||||
|
||||
def get_uptime(self):
|
||||
now = time.time()
|
||||
@@ -50,14 +52,14 @@ class Misc(commands.Cog):
|
||||
Info about the music player
|
||||
"""
|
||||
embed = discord.Embed(title='{music.title}', color=ctx.me.color)
|
||||
lavalink = self.bot.lavalink
|
||||
|
||||
listeners = 0
|
||||
for guild, player in lavalink.player_manager.players.items():
|
||||
listeners += len(player.listeners)
|
||||
if lavalink := self.bot.lavalink:
|
||||
for _, player in lavalink.player_manager.players.items():
|
||||
listeners += len(player.listeners)
|
||||
|
||||
embed.add_field(name='{music.players}', value=f'{len(lavalink.player_manager.players)}')
|
||||
embed.add_field(name='{music.listeners}', value=f'{listeners}')
|
||||
embed.add_field(name='{music.players}', value=f'{len(lavalink.player_manager.players)}')
|
||||
embed.add_field(name='{music.listeners}', value=f'{listeners}')
|
||||
embed = ctx.localizer.format_embed(embed)
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
|
||||
+224
-226
@@ -1,5 +1,7 @@
|
||||
# Discord Packages
|
||||
import discord
|
||||
import lavalink
|
||||
from discord import VoiceChannel
|
||||
from discord.ext import commands, tasks
|
||||
from lavalink.events import (
|
||||
NodeChangedEvent, NodeConnectedEvent, NodeDisconnectedEvent, PlayerUpdateEvent, QueueEndEvent, TrackEndEvent,
|
||||
@@ -9,32 +11,40 @@ from lavalink.models import AudioTrack
|
||||
import asyncio
|
||||
import json
|
||||
import re
|
||||
import urllib
|
||||
from typing import List
|
||||
import urllib.parse as urlparse
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from bot import MusicBot
|
||||
|
||||
# Bot Utilities
|
||||
from musicbot.utils.mixplayer.player import MixPlayer
|
||||
from ...utils import checks, thumbnailer, timeformatter
|
||||
from ...utils import checks, timeformatter
|
||||
from ...utils.thumbnailer import Thumbnailer
|
||||
from ...utils.userinteraction.paginators import QueuePaginator, TextPaginator
|
||||
from ...utils.userinteraction.scroller import ClearOn, Scroller
|
||||
from ...utils.userinteraction.selector import SelectMode, Selector, SelectorButton, SelectorItem
|
||||
from .decorators import BasicVoiceClient, require_playing, require_queue, require_voice_connection, voteable
|
||||
from .music_errors import WrongTextChannelError
|
||||
from .decorators import require_playing, require_queue, require_voice_connection, voteable
|
||||
from .music_errors import MusicError, PlayerNotAvailableError, WrongTextChannelError
|
||||
from .voice_client import BasicVoiceClient
|
||||
|
||||
time_rx = re.compile('[0-9]+')
|
||||
url_rx = re.compile('https?:\\/\\/(?:www\\.)?.+')
|
||||
|
||||
|
||||
class Music(commands.Cog):
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
def __init__(self, bot: MusicBot):
|
||||
self.bot: MusicBot = bot
|
||||
self.logger = self.bot.main_logger.bot_logger.getChild("Music")
|
||||
|
||||
self.bot = bot
|
||||
self.thumbnailer = Thumbnailer(bot=self.bot)
|
||||
|
||||
self.leave_timer.start()
|
||||
self.bot.lavalink.add_event_hook(self.track_hook)
|
||||
if self.bot.lavalink is None:
|
||||
raise MusicError("Lavalink is not yet initialized")
|
||||
self.lavalink: lavalink.Client = self.bot.lavalink
|
||||
self.lavalink.add_event_hook(self.track_hook)
|
||||
|
||||
async def cog_check(self, ctx):
|
||||
if not ctx.guild:
|
||||
@@ -47,30 +57,36 @@ class Music(commands.Cog):
|
||||
async def cog_before_invoke(self, ctx):
|
||||
""" Ensures a valid player exists whenever a command is run """
|
||||
# Creates a new only if one doesn't exist, ensures a valid player for all checks.
|
||||
self.bot.lavalink.player_manager.create(ctx.guild.id)
|
||||
if ctx.guild:
|
||||
self.lavalink.player_manager.create(ctx.guild.id)
|
||||
|
||||
async def enqueue(self, ctx, track, embed, silent=False, check_max_length=True) -> tuple[discord.Embed, bool]:
|
||||
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
def get_player(self, guild: discord.Guild) -> MixPlayer:
|
||||
player: Optional[MixPlayer] = self.lavalink.player_manager.get(guild.id)
|
||||
if player is None:
|
||||
raise PlayerNotAvailableError(f"Tried to get player for guild {guild.id} but got None")
|
||||
return player
|
||||
|
||||
async def enqueue(self, ctx, track: AudioTrack, embed, standalone: bool = False,
|
||||
check_max_length: bool = True) -> bool:
|
||||
player = self.get_player(ctx.guild)
|
||||
|
||||
track_duration_str = timeformatter.format_track_duration(track)
|
||||
|
||||
# Only add tracks that don't exceed the max track length
|
||||
if maxlength := self.max_track_length(ctx.guild, player):
|
||||
if track.duration > maxlength and check_max_length:
|
||||
if track.stream:
|
||||
length = '{live}'
|
||||
else:
|
||||
length = timeformatter.format_ms(track.duration)
|
||||
embed.description = ctx.localizer.format_str("{enqueue.toolong}",
|
||||
_length=length,
|
||||
_max=timeformatter.format_ms(maxlength))
|
||||
return embed, False
|
||||
maxlength = self.max_track_length(ctx.guild, player)
|
||||
if (track.stream or track.duration > maxlength) and check_max_length:
|
||||
embed.description = ctx.localizer.format_str("{enqueue.toolong}",
|
||||
_length=track_duration_str,
|
||||
_max=timeformatter.format_ms(maxlength))
|
||||
return False
|
||||
|
||||
track.extra["thumbnail_url"] = await thumbnailer.ThumbNailer.identify(self, track.identifier, track.uri)
|
||||
track.extra["thumbnail_url"] = await self.thumbnailer.identify(track.identifier, track.uri)
|
||||
track.requester = ctx.author.id
|
||||
|
||||
# Add to player
|
||||
track, pos_global, pos_local = player.add(requester=ctx.author.id, track=track)
|
||||
track, pos_global, pos_local = player.add(requester=ctx.author, track=track)
|
||||
|
||||
if player.current is not None and not silent:
|
||||
if player.current is not None and not standalone:
|
||||
if player.current.stream:
|
||||
until_play = '--:--'
|
||||
else:
|
||||
@@ -84,16 +100,11 @@ class Music(commands.Cog):
|
||||
if thumbnail_url := track.extra["thumbnail_url"]:
|
||||
embed.set_thumbnail(url=thumbnail_url)
|
||||
|
||||
if track.stream:
|
||||
duration = '{live}'
|
||||
else:
|
||||
duration = timeformatter.format_ms(int(track.duration))
|
||||
embed.description = f'[{track.title}]({track.uri})\n**{duration}**'
|
||||
|
||||
return embed, True
|
||||
embed.description = f'[{track.title}]({track.uri})\n**{track_duration_str}**'
|
||||
return True
|
||||
|
||||
def get_current_song_embed(self, ctx, include_time=False):
|
||||
player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player = self.get_player(ctx.guild)
|
||||
|
||||
if not player.current:
|
||||
return
|
||||
@@ -101,10 +112,7 @@ class Music(commands.Cog):
|
||||
song = f'**[{player.current.title}]({player.current.uri})**'
|
||||
if include_time:
|
||||
position = timeformatter.format_ms(player.position)
|
||||
if player.current.stream:
|
||||
duration = '{live}'
|
||||
else:
|
||||
duration = timeformatter.format_ms(player.current.duration)
|
||||
duration = timeformatter.format_track_duration(player.current)
|
||||
song += f'\n({position}/{duration})'
|
||||
|
||||
embed = discord.Embed(color=ctx.me.color, description=song, title='{now}')
|
||||
@@ -118,30 +126,28 @@ class Music(commands.Cog):
|
||||
embed = ctx.localizer.format_embed(embed)
|
||||
return embed
|
||||
|
||||
def max_track_length(self, guild, player):
|
||||
if maxlength := self.bot.settings.get(guild, 'duration.max', None):
|
||||
is_dynamic = self.bot.settings.get(guild, 'duration.is_dynamic', 'default_duration_type')
|
||||
listeners = max(1, len(player.listeners)) # Avoid division by 0
|
||||
def max_track_length(self, guild, player) -> float:
|
||||
# Numeric constants
|
||||
minimum_dynamic_length = 10
|
||||
minutes_to_milliseconds = 60 * 1000
|
||||
configured_max = self.bot.settings.get(guild, 'duration.max', float('inf'))
|
||||
is_dynamic = self.bot.settings.get(guild, 'duration.is_dynamic', 'default_duration_type')
|
||||
listeners = max(1, len(player.listeners)) # Avoid division by 0
|
||||
|
||||
if maxlength > 10 and is_dynamic:
|
||||
return max(maxlength*60*1000/listeners, 60*10*1000)
|
||||
else:
|
||||
return maxlength*60*1000
|
||||
if configured_max > minimum_dynamic_length and is_dynamic:
|
||||
return minutes_to_milliseconds * max(configured_max/listeners, minimum_dynamic_length)
|
||||
else:
|
||||
return None
|
||||
return minutes_to_milliseconds * configured_max
|
||||
|
||||
@commands.command(name='play')
|
||||
@require_voice_connection(should_connect=True)
|
||||
async def _play(self, ctx, *, query: str):
|
||||
""" Searches and plays a song from a given query. """
|
||||
async def _search_and_play_query(self, ctx, query: str, check_max_length: bool):
|
||||
self.logger.debug("Query: %s" % query)
|
||||
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player = self.get_player(ctx.guild)
|
||||
query = query.strip('<>')
|
||||
|
||||
if not url_rx.match(query):
|
||||
query = f'ytsearch:{query}'
|
||||
|
||||
results = await player.node.get_tracks(query)
|
||||
results: lavalink.LoadResult = await player.node.get_tracks(query)
|
||||
|
||||
if not results or not results.tracks:
|
||||
return await ctx.send(ctx.localizer.format_str("{nothing_found}"))
|
||||
@@ -149,17 +155,16 @@ class Music(commands.Cog):
|
||||
embed = discord.Embed(color=ctx.me.color)
|
||||
|
||||
if results.load_type == 'PLAYLIST_LOADED':
|
||||
numtracks = 0
|
||||
for track in results.tracks:
|
||||
_, track_added = await self.enqueue(ctx, track, embed, silent=True)
|
||||
if track_added:
|
||||
numtracks += 1
|
||||
|
||||
embed.title = '{playlist_enqued}'
|
||||
embed.description = f'{results.playlist_info.name} - {numtracks} {{tracks}}'
|
||||
tracks = [track for track in results.tracks if
|
||||
await self.enqueue(ctx, track, embed, True, check_max_length)]
|
||||
if tracks:
|
||||
embed.title = '{playlist_enqued}'
|
||||
embed.description = f'{results.playlist_info.name} - {len(tracks)} {{tracks}}'
|
||||
else:
|
||||
# TODO: This really means no tracks passed the max_length requirement
|
||||
return await ctx.send(ctx.localizer.format_str("{nothing_found}"))
|
||||
else:
|
||||
track = results.tracks[0]
|
||||
await self.enqueue(ctx, track, embed)
|
||||
await self.enqueue(ctx, results.tracks[0], embed, check_max_length=check_max_length)
|
||||
|
||||
embed = ctx.localizer.format_embed(embed)
|
||||
await ctx.send(embed=embed)
|
||||
@@ -167,19 +172,32 @@ class Music(commands.Cog):
|
||||
if not player.is_playing:
|
||||
await player.play()
|
||||
|
||||
@commands.command(name='play')
|
||||
@require_voice_connection(should_connect=True)
|
||||
async def _play(self, ctx, *, query: str):
|
||||
""" Searches and plays a song from a given query. """
|
||||
await self._search_and_play_query(ctx, query, check_max_length=True)
|
||||
|
||||
@commands.command(name='forceplay')
|
||||
@require_voice_connection(should_connect=True)
|
||||
@voteable(DJ_override=True, react_to_vote=True)
|
||||
async def _forceplay(self, ctx, *, query: str):
|
||||
""" Searches and plays a song from a given query, ignoring configured max duration. """
|
||||
await self._search_and_play_query(ctx, query, check_max_length=False)
|
||||
|
||||
@commands.command(name='seek')
|
||||
@checks.dj_or(alone=True, track_requester=True)
|
||||
@require_voice_connection()
|
||||
@require_playing(require_user_listening=True)
|
||||
async def _seek(self, ctx, *, time: str):
|
||||
""" Seeks to a given position in a track. """
|
||||
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player = self.get_player(ctx.guild)
|
||||
if seconds := time_rx.search(time):
|
||||
# Convert to milliseconds, include sign
|
||||
milliseconds = int(seconds.group())*1000 * (-1 if time.startswith('-1') else 1)
|
||||
|
||||
track_time = player.position + milliseconds
|
||||
await player.seek(track_time)
|
||||
await player.seek(int(track_time))
|
||||
msg = ctx.localizer.format_str("{seek.track_moved}", _position=timeformatter.format_ms(track_time))
|
||||
await ctx.send(msg)
|
||||
else:
|
||||
@@ -191,7 +209,7 @@ class Music(commands.Cog):
|
||||
@voteable(requester_override=True, react_to_vote=True)
|
||||
async def _skip(self, ctx):
|
||||
""" Skips the current track. """
|
||||
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player = self.get_player(ctx.guild)
|
||||
|
||||
await player.skip()
|
||||
embed = self.get_current_song_embed(ctx)
|
||||
@@ -202,7 +220,7 @@ class Music(commands.Cog):
|
||||
@require_playing(require_user_listening=True)
|
||||
async def _skip_to(self, ctx, pos: int = 1):
|
||||
""" Plays the queue from a specific point. Disregards tracks before the pos. """
|
||||
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player = self.get_player(ctx.guild)
|
||||
|
||||
# TODO: Do all queue out of range messages the same way
|
||||
if pos < 1:
|
||||
@@ -212,8 +230,11 @@ class Music(commands.Cog):
|
||||
|
||||
await player.skip(pos - 1)
|
||||
|
||||
msg = ctx.localizer.format_str("{skip_to.skipped_to}", _title=player.current.title, _pos=pos)
|
||||
await ctx.send(msg)
|
||||
if player.current:
|
||||
msg = ctx.localizer.format_str("{skip_to.skipped_to}", _title=player.current.title, _pos=pos)
|
||||
await ctx.send(msg)
|
||||
else:
|
||||
self.logger.error("Expected current track to be valid due to bounds checks")
|
||||
|
||||
@commands.command(name='stop')
|
||||
@require_voice_connection()
|
||||
@@ -221,7 +242,7 @@ class Music(commands.Cog):
|
||||
@voteable(DJ_override=True, react_to_vote=True)
|
||||
async def _stop(self, ctx):
|
||||
""" Stops the player and clears its queue. """
|
||||
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player = self.get_player(ctx.guild)
|
||||
player.queue.clear()
|
||||
await player.stop()
|
||||
|
||||
@@ -237,9 +258,9 @@ class Music(commands.Cog):
|
||||
|
||||
@commands.command(name='queue')
|
||||
@require_queue(require_member_queue=True)
|
||||
async def _queue(self, ctx, *, member: discord.Member = None):
|
||||
async def _queue(self, ctx, *, member: Optional[discord.Member] = None):
|
||||
""" Shows the player's queue. """
|
||||
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player = self.get_player(ctx.guild)
|
||||
pagified_queue = QueuePaginator(ctx.localizer, player, color=ctx.me.color, member=member)
|
||||
scroller = Scroller(ctx, pagified_queue)
|
||||
await scroller.start_scrolling(ClearOn.ManualExit | ClearOn.Timeout)
|
||||
@@ -256,7 +277,7 @@ class Music(commands.Cog):
|
||||
@require_playing()
|
||||
async def _pause(self, ctx):
|
||||
""" Pauses/Resumes the current track. """
|
||||
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player = self.get_player(ctx.guild)
|
||||
await player.set_pause(not player.paused)
|
||||
if player.paused:
|
||||
await ctx.send(ctx.localizer.format_str("{resume.paused}"))
|
||||
@@ -268,8 +289,8 @@ class Music(commands.Cog):
|
||||
@require_queue(require_author_queue=True)
|
||||
async def _shuffle(self, ctx):
|
||||
""" Shuffles your queue. """
|
||||
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player.shuffle_user_queue(ctx.author.id)
|
||||
player = self.get_player(ctx.guild)
|
||||
player.shuffle_user_queue(ctx.author)
|
||||
await ctx.send(ctx.localizer.format_str("{shuffle}"))
|
||||
|
||||
@commands.command(name='move')
|
||||
@@ -277,10 +298,10 @@ class Music(commands.Cog):
|
||||
@require_queue(require_author_queue=True)
|
||||
async def _move(self, ctx):
|
||||
""" Moves a song in your queue. """
|
||||
player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player = self.get_player(ctx.guild)
|
||||
|
||||
# We create a new selector for each selection
|
||||
def move_selector(ctx, queue: List[AudioTrack], title: str, first: bool):
|
||||
def build_move_selector(ctx, queue: List[AudioTrack], title: str, first: bool):
|
||||
async def return_track(track):
|
||||
return track
|
||||
|
||||
@@ -302,30 +323,30 @@ class Music(commands.Cog):
|
||||
page = 0
|
||||
while True:
|
||||
# Prompt the user for which track to move
|
||||
selector = move_selector(ctx, player.user_queue(ctx.author.id), "{moved.choose_pos}", True)
|
||||
message, track_to_move = await selector.start_scrolling(ClearOn.Timeout, message, page)
|
||||
selector = build_move_selector(ctx, player.user_queue(ctx.author), "{moved.choose_pos}", True)
|
||||
message, timed_out, track_to_move = await selector.start_scrolling(ClearOn.Timeout, message, page)
|
||||
page = selector.page_number
|
||||
|
||||
if not track_to_move:
|
||||
if not track_to_move or timed_out:
|
||||
break
|
||||
|
||||
# Prompt the user for where to move it
|
||||
selector = move_selector(ctx, player.user_queue(ctx.author.id), "{moved.choose_song}", False)
|
||||
message, track_to_replace = await selector.start_scrolling(ClearOn.Timeout, message, page)
|
||||
selector = build_move_selector(ctx, player.user_queue(ctx.author), "{moved.choose_song}", False)
|
||||
message, timed_out, track_to_replace = await selector.start_scrolling(ClearOn.Timeout, message, page)
|
||||
page = selector.page_number
|
||||
|
||||
if not track_to_replace:
|
||||
if not track_to_replace or timed_out:
|
||||
break
|
||||
|
||||
# At this point the user has chosen two songs, we will move the first song to be in front of the second
|
||||
# one in the queue. Find the index of both songs in the current queue
|
||||
|
||||
# Keep updating the user queue in case it changes while the user is selecting songs
|
||||
user_queue = player.user_queue(ctx.author.id)
|
||||
user_queue = player.user_queue(ctx.author)
|
||||
try:
|
||||
pos_initial = user_queue.index(track_to_move[0])
|
||||
pos_final = user_queue.index(track_to_replace[0])
|
||||
player.move_user_track(ctx.author.id, pos_initial, pos_final)
|
||||
player.move_user_track(ctx.author, pos_initial, pos_final)
|
||||
except ValueError:
|
||||
# Track not found in queue. Queue could have changed during selection
|
||||
# if a song was skipped or the current song finished
|
||||
@@ -334,17 +355,19 @@ class Music(commands.Cog):
|
||||
if message:
|
||||
await message.delete()
|
||||
|
||||
@commands.command(name='remove')
|
||||
@require_voice_connection()
|
||||
@require_queue(require_author_queue=True)
|
||||
async def _remove(self, ctx):
|
||||
player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
|
||||
user_queue: List[AudioTrack] = player.user_queue(ctx.author.id)
|
||||
async def _interactive_remove(self, ctx, queue: List[AudioTrack]):
|
||||
"""
|
||||
Helper function for creating an interactive selector over a given queue
|
||||
in which will remove the selected tracks on exit.
|
||||
"""
|
||||
player = self.get_player(ctx.guild)
|
||||
|
||||
tracks_to_remove = []
|
||||
|
||||
async def update_remove_list(tracks_list, track):
|
||||
async def update_remove_list(tracks_list, track: AudioTrack):
|
||||
# It seems duplicate songs still don't satisfy equality
|
||||
# which meas remove is sufficient to preserve order
|
||||
# of similar items
|
||||
if track in tracks_list:
|
||||
tracks_list.remove(track)
|
||||
else:
|
||||
@@ -363,59 +386,55 @@ class Music(commands.Cog):
|
||||
return inner
|
||||
|
||||
selector_buttons = []
|
||||
for index, track in enumerate(user_queue, start=1):
|
||||
# Build each selection from the queue, a visible string and a callback.
|
||||
for index, track in enumerate(queue, start=1):
|
||||
requester = self.bot.get_user(track.requester)
|
||||
selector_buttons.append(
|
||||
SelectorItem(f'`{index}` [{track.title}]({track.uri})', str(index),
|
||||
add_change_button_color(update_remove_list, tracks_to_remove, track)))
|
||||
SelectorItem(f'`{index}` [{track.title}]({track.uri}) - {requester.mention if requester else ""}',
|
||||
str(index), add_change_button_color(update_remove_list, tracks_to_remove, track)))
|
||||
|
||||
remove_selector = Selector(ctx, selector_buttons, select_mode=SelectMode.MultiSelect,
|
||||
use_tick_for_stop_emoji=True, color=ctx.me.color, title='Select songs to remove')
|
||||
await remove_selector.start_scrolling(ClearOn.AnyExit)
|
||||
_, timed_out, _ = await remove_selector.start_scrolling(ClearOn.AnyExit)
|
||||
|
||||
# If any tracks were removed create a scroller for navigating them
|
||||
if tracks_to_remove:
|
||||
if tracks_to_remove and not timed_out:
|
||||
paginator = TextPaginator(color=ctx.me.color, title="Removed")
|
||||
|
||||
# If the user spent a long time selecting songs the queue might have changed
|
||||
user_queue = player.user_queue(ctx.author.id)
|
||||
track_indexes_to_remove = [i for i, track in enumerate(user_queue) if track in tracks_to_remove]
|
||||
tracks_removed: List[Tuple[int, str]] = []
|
||||
for track in tracks_to_remove:
|
||||
if remove_result := player.remove_track(track):
|
||||
(position, removed_track) = remove_result
|
||||
if requester := self.bot.get_user(removed_track.requester):
|
||||
tracks_removed.append((position, f"{removed_track.title} - {requester.mention}"))
|
||||
|
||||
# Sort in reverse order since songs shift index when deleting
|
||||
tracks_removed = []
|
||||
for track in sorted(track_indexes_to_remove, reverse=True):
|
||||
if removed_track := player.remove_user_track(ctx.author.id, track):
|
||||
tracks_removed.append(f"{removed_track.title}")
|
||||
|
||||
# We want to display the removed tracks in the original queue order, so reverse the list once again
|
||||
for removed in reversed(tracks_removed):
|
||||
paginator.add_line(removed)
|
||||
# remove_track returns the global queue index of the track,
|
||||
# so we display the removed tracks in "queue order"
|
||||
for removed in sorted(tracks_removed, key=lambda x: x[0]):
|
||||
paginator.add_line(removed[1])
|
||||
|
||||
paginator.close_page()
|
||||
scroller = Scroller(ctx, paginator)
|
||||
await scroller.start_scrolling(ClearOn.ManualExit | ClearOn.Timeout)
|
||||
|
||||
@commands.command(name='remove')
|
||||
@require_voice_connection()
|
||||
@require_queue(require_author_queue=True)
|
||||
async def _remove(self, ctx):
|
||||
""" Remove a song from your queue. """
|
||||
player = self.get_player(ctx.guild)
|
||||
return await self._interactive_remove(ctx, player.user_queue(ctx.author))
|
||||
|
||||
@commands.command(name="DJremove")
|
||||
@checks.dj_or()
|
||||
@require_voice_connection()
|
||||
@require_queue()
|
||||
async def _djremove(self, ctx, pos: int, member: discord.Member = None):
|
||||
async def _djremove(self, ctx, member: Optional[discord.Member] = None):
|
||||
""" Remove a song from either the global queue or a users queue"""
|
||||
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
|
||||
player = self.get_player(ctx.guild)
|
||||
# TODO: add way to allow "member" as regular arg in require_queue() instead of only kwargs
|
||||
queue = player.user_queue(member.id) if member else player.queue
|
||||
|
||||
# TODO: Do all queue out of range messages the same way
|
||||
if pos <= len(queue) and pos >= 1:
|
||||
if member is None:
|
||||
removed = player.remove_global_track(pos - 1)
|
||||
else:
|
||||
removed = player.remove_user_track(member.id, pos - 1)
|
||||
|
||||
requester = self.bot.get_user(removed.requester)
|
||||
await ctx.send(ctx.localizer.format_str("{dj_removed}", _title=removed.title, _user=requester.display_name))
|
||||
else:
|
||||
await ctx.send(ctx.localizer.format_str("{out_of_range}", _len=len(queue)))
|
||||
queue = player.user_queue(member) if member else player.global_queue()
|
||||
return await self._interactive_remove(ctx, queue)
|
||||
|
||||
@commands.command(name='removeuser')
|
||||
@checks.dj_or(alone=True)
|
||||
@@ -423,8 +442,8 @@ class Music(commands.Cog):
|
||||
@require_queue(require_member_queue=True)
|
||||
async def _user_queue_remove(self, ctx, *, member: discord.Member):
|
||||
""" Remove a song from either the global queue or a users queue"""
|
||||
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player.remove_user_queue(member.id)
|
||||
player = self.get_player(ctx.guild)
|
||||
player.remove_user_queue(member)
|
||||
|
||||
embed = discord.Embed(description="{dj_remove_user}", color=ctx.me.color)
|
||||
embed = ctx.localizer.format_embed(embed, _id=member.id)
|
||||
@@ -434,49 +453,47 @@ class Music(commands.Cog):
|
||||
@commands.command(name='search')
|
||||
@require_voice_connection(should_connect=True)
|
||||
async def _search(self, ctx, *, query):
|
||||
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player = self.get_player(ctx.guild)
|
||||
|
||||
self.logger.debug("Query: %s" % query)
|
||||
if not query.startswith('ytsearch:') and not query.startswith('scsearch:'):
|
||||
query = 'ytsearch:' + query
|
||||
|
||||
results = await player.node.get_tracks(query)
|
||||
|
||||
embed = discord.Embed(description='{nothing_found}', color=0x36393F)
|
||||
if not results or not results['tracks']:
|
||||
embed = discord.Embed(description='{nothing_found}', color=0x36393F)
|
||||
embed = ctx.localizer.format_embed(embed)
|
||||
return await ctx.send(embed=embed)
|
||||
|
||||
# This is the base embed that will be modified by the selector
|
||||
embed = discord.Embed(color=ctx.me.color)
|
||||
buttons = []
|
||||
for i, track in enumerate(results['tracks'], start=1):
|
||||
duration = timeformatter.format_ms(int(track.duration))
|
||||
interaction = SelectorItem(f'`{i}` [{track.title}]({track.uri}) `{duration}`', str(i),
|
||||
wrap_in_button_callback(
|
||||
self.enqueue, ctx, track, discord.Embed(color=ctx.me.color)))
|
||||
self.enqueue, ctx, track, embed))
|
||||
buttons.append(interaction)
|
||||
|
||||
search_selector = Selector(ctx, buttons, select_mode=SelectMode.SingleSelect,
|
||||
color=ctx.me.color, title=ctx.localizer.format_str('{results}'))
|
||||
message, callback_results = await search_selector.start_scrolling()
|
||||
message, timed_out, _ = await search_selector.start_scrolling(ClearOn.Timeout)
|
||||
|
||||
if len(callback_results) == 0:
|
||||
if message:
|
||||
await message.delete()
|
||||
if timed_out:
|
||||
return
|
||||
|
||||
song_embed, song_added = callback_results[0]
|
||||
if song_added:
|
||||
embed = ctx.localizer.format_embed(song_embed)
|
||||
await message.edit(embed=embed, view=None)
|
||||
await message.edit(embed=ctx.localizer.format_embed(embed), view=None)
|
||||
|
||||
if not player.is_playing:
|
||||
await player.play()
|
||||
if not player.is_playing:
|
||||
await player.play()
|
||||
|
||||
@commands.command(name='disconnect')
|
||||
@require_voice_connection()
|
||||
@voteable(DJ_override=True, react_to_vote=True)
|
||||
async def _disconnect(self, ctx):
|
||||
""" Disconnects the player from the voice channel and clears its queue. """
|
||||
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player = self.get_player(ctx.guild)
|
||||
|
||||
player.queue.clear()
|
||||
await player.stop()
|
||||
@@ -490,10 +507,10 @@ class Music(commands.Cog):
|
||||
@voteable(DJ_override=True, react_to_vote=True)
|
||||
async def _reconnect(self, ctx):
|
||||
""" Tries to disconnect then reconnect the player in case the bot gets stuck on a song """
|
||||
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player = self.get_player(ctx.guild)
|
||||
current_channel = player.channel_id
|
||||
|
||||
async def recon():
|
||||
async def inner_reconnect():
|
||||
await player.stop()
|
||||
if ctx.voice_client:
|
||||
await ctx.voice_client.disconnect()
|
||||
@@ -504,18 +521,18 @@ class Music(commands.Cog):
|
||||
if player.current:
|
||||
track = player.current
|
||||
start_time = player.position
|
||||
await recon()
|
||||
await inner_reconnect()
|
||||
if track:
|
||||
await player.play(track, start_time=start_time)
|
||||
await player.play(track, start_time=int(start_time))
|
||||
else:
|
||||
await recon()
|
||||
await inner_reconnect()
|
||||
|
||||
@commands.command(name='volume')
|
||||
@checks.dj_or(alone=True, track_requester=True)
|
||||
@require_playing(require_user_listening=True)
|
||||
async def _volume(self, ctx, volume: int = None):
|
||||
async def _volume(self, ctx, volume: Optional[int] = None):
|
||||
""" Changes the player's volume. Must be between 0 and 1000. Error Handling for that is done by Lavalink. """
|
||||
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player = self.get_player(ctx.guild)
|
||||
|
||||
if not volume:
|
||||
return await ctx.send(f'🔈 | {player.volume}%')
|
||||
@@ -531,53 +548,17 @@ class Music(commands.Cog):
|
||||
embed = ctx.localizer.format_embed(embed, _volume=player.volume)
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@commands.command(name='forceplay')
|
||||
@require_voice_connection(should_connect=True)
|
||||
@voteable(DJ_override=True, react_to_vote=True)
|
||||
async def _forceplay(self, ctx, *, query: str):
|
||||
""" Searches and plays a song from a given query. """
|
||||
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
query = query.strip('<>')
|
||||
|
||||
if not url_rx.match(query):
|
||||
query = f'ytsearch:{query}'
|
||||
|
||||
results = await player.node.get_tracks(query)
|
||||
|
||||
if not results or not results.tracks:
|
||||
return await ctx.send(ctx.localizer.format_str("{nothing_found}"))
|
||||
|
||||
embed = discord.Embed(color=ctx.me.color)
|
||||
|
||||
if results.load_type == 'PLAYLIST_LOADED':
|
||||
numtracks = 0
|
||||
for track in results.tracks:
|
||||
_, track_added = await self.enqueue(ctx, track, embed, silent=True, check_max_length=False)
|
||||
if track_added:
|
||||
numtracks += 1
|
||||
|
||||
embed.title = '{playlist_enqued}'
|
||||
embed.description = f'{results.playlist_info.name} - {numtracks} {{tracks}}'
|
||||
else:
|
||||
track = results.tracks[0]
|
||||
await self.enqueue(ctx, track, embed, check_max_length=False)
|
||||
|
||||
embed = ctx.localizer.format_embed(embed)
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
if not player.is_playing:
|
||||
await player.play()
|
||||
|
||||
@commands.command(name='forcedisconnect')
|
||||
@checks.dj_or(alone=True)
|
||||
async def _forcedisconnect(self, ctx):
|
||||
""" Attempts to force disconnect the bot without checking if it is connected initially. """
|
||||
try:
|
||||
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player = self.get_player(ctx.guild)
|
||||
player.queue.clear()
|
||||
await player.stop()
|
||||
except Exception:
|
||||
self.logger.exception("Error forcedisconnecting")
|
||||
except Exception as e:
|
||||
self.logger.error("Error forcedisconnecting")
|
||||
self.logger.exception(e)
|
||||
await ctx.voice_client.disconnect(force=True)
|
||||
embed = discord.Embed(description='{disconnect.disconnected}', color=ctx.me.color)
|
||||
embed = ctx.localizer.format_embed(embed)
|
||||
@@ -588,10 +569,11 @@ class Music(commands.Cog):
|
||||
@require_voice_connection()
|
||||
async def _normalize(self, ctx):
|
||||
""" Reset the equalizer and volume """
|
||||
player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player = self.get_player(ctx.guild)
|
||||
|
||||
await player.set_volume(100)
|
||||
await player.bassboost(False)
|
||||
await player.nightcoreify(False)
|
||||
|
||||
embed = discord.Embed(description='{volume.reset}', color=ctx.me.color)
|
||||
embed = ctx.localizer.format_embed(embed)
|
||||
@@ -601,7 +583,7 @@ class Music(commands.Cog):
|
||||
@checks.dj_or(alone=True)
|
||||
async def _boost(self, ctx, boost: bool = False):
|
||||
""" Set the equalizer to bass boost the music """
|
||||
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player = self.get_player(ctx.guild)
|
||||
|
||||
if boost is not None:
|
||||
await player.bassboost(boost)
|
||||
@@ -616,7 +598,7 @@ class Music(commands.Cog):
|
||||
@checks.dj_or(alone=True)
|
||||
async def _nightcore(self, ctx, boost: bool = False):
|
||||
""" Set a filter mimicking nightcore the music """
|
||||
player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player = self.get_player(ctx.guild)
|
||||
|
||||
await player.nightcoreify(boost)
|
||||
|
||||
@@ -628,7 +610,7 @@ class Music(commands.Cog):
|
||||
@commands.command(name='history')
|
||||
async def _history(self, ctx):
|
||||
""" Show the last 10 songs played """
|
||||
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player = self.get_player(ctx.guild)
|
||||
history = player.get_history()
|
||||
if not history:
|
||||
embed = discord.Embed(description='{history.empty}', color=ctx.me.color)
|
||||
@@ -645,8 +627,9 @@ class Music(commands.Cog):
|
||||
embed = discord.Embed(title=ctx.localizer.format_str('{history.title}'), color=ctx.me.color,
|
||||
description=description)
|
||||
|
||||
if thumbnail_url := player.current.extra["thumbnail_url"]:
|
||||
embed.set_thumbnail(url=thumbnail_url)
|
||||
if current := player.current:
|
||||
if thumbnail_url := current.extra["thumbnail_url"]:
|
||||
embed.set_thumbnail(url=thumbnail_url)
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@commands.cooldown(1, 5, commands.BucketType.guild)
|
||||
@@ -654,7 +637,7 @@ class Music(commands.Cog):
|
||||
async def _lyrics(self, ctx, *query: str):
|
||||
|
||||
# TODO:
|
||||
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player = self.get_player(ctx.guild)
|
||||
|
||||
genius_access_token = self.bot.APIkeys.get('genius', None)
|
||||
|
||||
@@ -664,13 +647,13 @@ class Music(commands.Cog):
|
||||
excluded_words = ['music', 'video', 'version', 'original', 'lyrics', 'lyric',
|
||||
'official', 'live', 'instrumental', 'audio', 'hd']
|
||||
|
||||
query = ' '.join(query)
|
||||
query_as_string = ' '.join(query)
|
||||
|
||||
if not query and player.is_playing:
|
||||
query = player.current.title
|
||||
query = re.sub(r'[-()_\[\]]', '', query)
|
||||
filtered_words = [word for word in query.split() if word.lower() not in excluded_words]
|
||||
query = ' '.join(filtered_words)
|
||||
if not query_as_string and player.is_playing and player.current:
|
||||
query_as_string = player.current.title
|
||||
query_as_string = re.sub(r'[-()_\[\]]', '', query_as_string)
|
||||
filtered_words = [word for word in query_as_string.split() if word.lower() not in excluded_words]
|
||||
query_as_string = ' '.join(filtered_words)
|
||||
|
||||
async def get_site_content(url):
|
||||
async with self.bot.session.get(url) as resp:
|
||||
@@ -681,8 +664,8 @@ class Music(commands.Cog):
|
||||
status_msg = await ctx.send(embed=embed)
|
||||
|
||||
try:
|
||||
url = 'https://api.genius.com/search?' + urllib.parse.urlencode(
|
||||
{'access_token': genius_access_token, 'q': query})
|
||||
url = 'https://api.genius.com/search?' + urlparse.urlencode(
|
||||
{'access_token': genius_access_token, 'q': query_as_string})
|
||||
result = await get_site_content(url)
|
||||
response = json.loads(result)
|
||||
song_id = response['response']['hits'][0]['result']['id']
|
||||
@@ -713,7 +696,7 @@ class Music(commands.Cog):
|
||||
await ctx.send(embed=page)
|
||||
else:
|
||||
paginator.add_page_indicator(ctx.localizer)
|
||||
await Scroller(ctx, paginator).start_scrolling(ClearOn.AnyExi)
|
||||
await Scroller(ctx, paginator).start_scrolling(ClearOn.AnyExit)
|
||||
|
||||
@commands.command(name='scrub')
|
||||
@checks.dj_or(alone=True)
|
||||
@@ -721,7 +704,7 @@ class Music(commands.Cog):
|
||||
@require_playing(require_user_listening=True)
|
||||
async def _scrub(self, ctx):
|
||||
""" Shows a set of controls which can be used to skip forward or backwards in the song """
|
||||
player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player = self.get_player(ctx.guild)
|
||||
controls = '{scrub.controls}'
|
||||
|
||||
def seek_callback(player: MixPlayer, seconds):
|
||||
@@ -753,7 +736,7 @@ class Music(commands.Cog):
|
||||
async def _loop(self, ctx):
|
||||
# This is done using subcommands to have separate vote counts for starting and stopping.
|
||||
if ctx.invoked_subcommand is None:
|
||||
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player = self.get_player(ctx.guild)
|
||||
embed = discord.Embed(color=ctx.me.color, title='Loop status')
|
||||
|
||||
if player.looping:
|
||||
@@ -767,7 +750,7 @@ class Music(commands.Cog):
|
||||
@voteable(DJ_override=True, react_to_vote=True)
|
||||
async def _loop_start(self, ctx):
|
||||
""" Set the equalizer to bass boost the music """
|
||||
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player = self.get_player(ctx.guild)
|
||||
player.enable_looping(True)
|
||||
embed = discord.Embed(color=ctx.me.color)
|
||||
embed.description = 'Looping turned on'
|
||||
@@ -779,7 +762,7 @@ class Music(commands.Cog):
|
||||
@voteable(DJ_override=True, react_to_vote=True)
|
||||
async def _loop_stop(self, ctx):
|
||||
""" Set the equalizer to bass boost the music """
|
||||
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player = self.get_player(ctx.guild)
|
||||
player.enable_looping(False)
|
||||
embed = discord.Embed(color=ctx.me.color)
|
||||
embed.description = 'Looping turned off'
|
||||
@@ -787,19 +770,21 @@ class Music(commands.Cog):
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
async def cog_unload(self):
|
||||
self.bot.lavalink._event_hooks.clear()
|
||||
self.lavalink._event_hooks.clear()
|
||||
|
||||
async def track_hook(self, event):
|
||||
if isinstance(event, TrackEndEvent):
|
||||
channel = self.bot.get_channel(event.player.fetch('channel'))
|
||||
player = self.bot.lavalink.player_manager.get(channel.guild.id)
|
||||
player.skip_voters.clear()
|
||||
if channel := self.bot.get_channel(event.player.fetch('channel')):
|
||||
if isinstance(channel, VoiceChannel):
|
||||
player = self.get_player(channel.guild)
|
||||
player.skip_voters.clear()
|
||||
|
||||
if isinstance(event, TrackStartEvent):
|
||||
pass
|
||||
if isinstance(event, QueueEndEvent):
|
||||
channel = self.bot.get_channel(event.player.fetch('channel'))
|
||||
await self.check_leave_voice(channel.guild)
|
||||
if channel := self.bot.get_channel(event.player.fetch('channel')):
|
||||
if isinstance(channel, VoiceChannel):
|
||||
await self.check_leave_voice(channel.guild)
|
||||
if isinstance(event, PlayerUpdateEvent):
|
||||
pass
|
||||
if isinstance(event, NodeDisconnectedEvent):
|
||||
@@ -812,44 +797,57 @@ class Music(commands.Cog):
|
||||
pass
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_voice_state_update(self, member, before, after):
|
||||
async def on_voice_state_update(self, member: discord.Member, _: discord.VoiceState, after: discord.VoiceState):
|
||||
""" Updates listeners when the bot or a user changes voice state """
|
||||
if self.bot.user is None:
|
||||
return # Bot not logged in
|
||||
if member.id == self.bot.user.id and after.channel is not None:
|
||||
voice_channel = after.channel
|
||||
player = self.bot.lavalink.player_manager.get(member.guild.id)
|
||||
try:
|
||||
player = self.get_player(member.guild)
|
||||
except PlayerNotAvailableError: # This is expected if we have not created a player for the guild yet
|
||||
return
|
||||
player.clear_listeners()
|
||||
for member in voice_channel.members:
|
||||
if not member.bot:
|
||||
player.update_listeners(member, member.voice)
|
||||
|
||||
if not member.bot:
|
||||
player = self.bot.lavalink.player_manager.get(member.guild.id)
|
||||
if player is not None:
|
||||
player.update_listeners(member, after)
|
||||
await self.check_leave_voice(member.guild)
|
||||
try:
|
||||
player = self.get_player(member.guild)
|
||||
except PlayerNotAvailableError: # This is expected if we have not created a player for the guild yet
|
||||
return
|
||||
player.update_listeners(member, after)
|
||||
await self.check_leave_voice(member.guild)
|
||||
|
||||
async def check_leave_voice(self, guild):
|
||||
async def check_leave_voice(self, guild: discord.Guild):
|
||||
""" Checks if the bot should leave the voice channel """
|
||||
# TODO, disconnect timer?
|
||||
player = self.bot.lavalink.player_manager.get(guild.id)
|
||||
player = self.get_player(guild)
|
||||
if len(player.listeners) == 0 and player.is_connected:
|
||||
if player.queue.empty and player.current is None:
|
||||
await player.stop()
|
||||
await guild.voice_client.disconnect()
|
||||
if voice_client := guild.voice_client:
|
||||
await voice_client.disconnect(force=False)
|
||||
|
||||
async def leave_check(self):
|
||||
for player_id in self.bot.lavalink.player_manager.players:
|
||||
await self.check_leave_voice(self.bot.get_guild(player_id))
|
||||
for player_id in self.lavalink.player_manager.players:
|
||||
if guild := self.bot.get_guild(player_id):
|
||||
await self.check_leave_voice(guild)
|
||||
|
||||
@tasks.loop(seconds=10.0)
|
||||
async def leave_timer(self):
|
||||
try:
|
||||
await self.leave_check()
|
||||
except Exception as err:
|
||||
self.logger.debug("Error in leave_timer loop.\nTraceback: %s" % (err))
|
||||
self.logger.error("Error in leave_timer loop")
|
||||
self.logger.exception(err)
|
||||
|
||||
|
||||
def wrap_in_button_callback(func, *args):
|
||||
"""
|
||||
Take a callback and convert it to the form required by a SelectorItem
|
||||
"""
|
||||
async def inner(_interaction, _button):
|
||||
return await func(*args)
|
||||
return inner
|
||||
|
||||
@@ -1,46 +1,16 @@
|
||||
# Discord Packages
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
import asyncio
|
||||
import functools
|
||||
import inspect
|
||||
import math
|
||||
|
||||
# Bot Utilities
|
||||
from musicbot.utils.mixplayer.player import MixPlayer
|
||||
from ...utils.checks import is_dj
|
||||
from .music_errors import WrongVoiceChannelError
|
||||
|
||||
|
||||
class BasicVoiceClient(discord.VoiceClient):
|
||||
def __init__(self, client: discord.Client, channel: discord.abc.Connectable):
|
||||
self.client = client
|
||||
self.channel = channel
|
||||
if hasattr(self.client, 'lavalink'):
|
||||
self.lavalink = self.client.lavalink
|
||||
else:
|
||||
raise Exception("client did not have defined lavalink before connect")
|
||||
|
||||
async def on_voice_server_update(self, data):
|
||||
await self.lavalink.voice_update_handler({'t': 'VOICE_SERVER_UPDATE', 'd': data})
|
||||
|
||||
async def on_voice_state_update(self, data):
|
||||
await self.lavalink.voice_update_handler({'t': 'VOICE_STATE_UPDATE', 'd': data})
|
||||
|
||||
async def connect(self, *, timeout: float, reconnect: bool, self_deaf: bool = False,
|
||||
self_mute: bool = False) -> None:
|
||||
await self.channel.guild.change_voice_state(channel=self.channel, self_mute=self_mute, self_deaf=self_deaf)
|
||||
|
||||
async def disconnect(self, *, force: bool = False) -> None:
|
||||
player = self.lavalink.player_manager.get(self.channel.guild.id)
|
||||
|
||||
# no need to disconnect if we are not connected
|
||||
if not force and not player.is_connected:
|
||||
return
|
||||
|
||||
# None means disconnect
|
||||
await self.channel.guild.change_voice_state(channel=None)
|
||||
player.channel_id = None
|
||||
self.cleanup()
|
||||
from . import music_errors
|
||||
from .voice_client import BasicVoiceClient
|
||||
|
||||
|
||||
def require_voice_connection(should_connect=False):
|
||||
@@ -52,37 +22,39 @@ def require_voice_connection(should_connect=False):
|
||||
@functools.wraps(func)
|
||||
async def ensure_voice_inner(self, ctx, *command_args, **kwargs):
|
||||
""" This check ensures that the bot and command author are in the same voicechannel. """
|
||||
player: MixPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
if not player:
|
||||
raise music_errors.MusicError("ensure voice could not get lavalink player")
|
||||
|
||||
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
if not ctx.author.voice or not ctx.author.voice.channel:
|
||||
raise commands.CommandInvokeError('Join a voicechannel first.')
|
||||
raise music_errors.UserNotConnectedError('Join a voicechannel first.')
|
||||
|
||||
if not player.is_connected:
|
||||
if not should_connect:
|
||||
raise commands.CommandInvokeError('Not connected.')
|
||||
raise music_errors.BotNotConnectedError('Not connected.')
|
||||
|
||||
user_voice_channel = ctx.author.voice.channel
|
||||
permissions = user_voice_channel.permissions_for(ctx.me)
|
||||
|
||||
if not permissions.connect or not permissions.speak: # Check user limit too?
|
||||
raise commands.CommandInvokeError('I need the `CONNECT` and `SPEAK` permissions.')
|
||||
raise music_errors.MissingPermissionsError('I need the `CONNECT` and `SPEAK` permissions.')
|
||||
|
||||
if (len(user_voice_channel.members) == user_voice_channel.user_limit) and not permissions.administrator:
|
||||
raise commands.CommandInvokeError('The channel is currently full')
|
||||
raise music_errors.VoiceChannelFullError('The channel is currently full', user_voice_channel)
|
||||
|
||||
# Check against music channel restrictions in bot settings
|
||||
if voice_channels := self.bot.settings.get(ctx.guild, 'channels.music', []):
|
||||
if user_voice_channel.id not in voice_channels:
|
||||
raise WrongVoiceChannelError(
|
||||
raise music_errors.WrongVoiceChannelError(
|
||||
'You need to be in the right voice channel', channels=voice_channels)
|
||||
|
||||
player.store('channel', ctx.channel.id)
|
||||
await ctx.author.voice.channel.connect(cls=BasicVoiceClient)
|
||||
|
||||
elif int(player.channel_id) != ctx.author.voice.channel.id:
|
||||
elif player.channel_id and int(player.channel_id) != ctx.author.voice.channel.id:
|
||||
bot_channel = self.bot.get_channel(int(player.channel_id))
|
||||
raise WrongVoiceChannelError(
|
||||
'You need to be in my voice channel', channels=[bot_channel])
|
||||
raise music_errors.UserInDifferentVoiceChannelError('You need to be in my voice channel',
|
||||
channel=bot_channel)
|
||||
|
||||
await func(self, ctx, *command_args, **kwargs)
|
||||
|
||||
@@ -102,10 +74,10 @@ def require_playing(require_user_listening=False):
|
||||
if player := self.bot.lavalink.player_manager.get(ctx.guild.id):
|
||||
|
||||
if not player.is_playing:
|
||||
raise commands.CommandInvokeError('Not playing')
|
||||
raise music_errors.RequirePlayingError('Not playing')
|
||||
|
||||
if require_user_listening and (ctx.author not in player.listeners):
|
||||
raise commands.CommandInvokeError('Not listening')
|
||||
raise music_errors.RequireListeningError('Not listening')
|
||||
|
||||
else:
|
||||
raise Exception('Player object does not yet exist')
|
||||
@@ -136,7 +108,7 @@ def require_queue(require_member_queue=False, require_author_queue=False):
|
||||
if require_member_queue:
|
||||
try:
|
||||
if member := kwargs['member']: # Ignore if member is None
|
||||
user_queue = player.user_queue(member.id)
|
||||
user_queue = player.user_queue(member)
|
||||
if not user_queue:
|
||||
embed = discord.Embed(description='{queue.user_empty}', color=ctx.me.color)
|
||||
embed = ctx.localizer.format_embed(embed, _user=member.display_name)
|
||||
@@ -145,7 +117,7 @@ def require_queue(require_member_queue=False, require_author_queue=False):
|
||||
raise Exception("require_member_error can only be used on commands with a member keyword argument")
|
||||
|
||||
if require_author_queue:
|
||||
user_queue = player.user_queue(ctx.author.id)
|
||||
user_queue = player.user_queue(ctx.author)
|
||||
if not user_queue:
|
||||
embed = discord.Embed(description='{my_queue}', color=ctx.me.color)
|
||||
embed = ctx.localizer.format_embed(embed)
|
||||
|
||||
@@ -1,20 +1,77 @@
|
||||
# define Python user-defined exceptions
|
||||
# Discord Packages
|
||||
from discord.ext.commands import CommandError
|
||||
|
||||
from typing import Optional
|
||||
|
||||
class WrongChannelError(CommandError):
|
||||
def __init__(self, e: str, channels=None):
|
||||
self.original = e
|
||||
|
||||
class MusicError(CommandError):
|
||||
def __init__(self, message: str, inner_error: Optional[Exception] = None):
|
||||
self.inner_error = inner_error
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class PlayerNotAvailableError(MusicError):
|
||||
def __init__(self, message: str, inner_error: Optional[Exception] = None):
|
||||
super().__init__(message, inner_error=inner_error)
|
||||
|
||||
|
||||
class MissingPermissionsError(MusicError):
|
||||
def __init__(self, message: str, inner_error: Optional[Exception] = None):
|
||||
super().__init__(message, inner_error=inner_error)
|
||||
|
||||
|
||||
class BotNotConnectedError(MusicError):
|
||||
def __init__(self, message: str, inner_error: Optional[Exception] = None):
|
||||
super().__init__(message, inner_error=inner_error)
|
||||
|
||||
|
||||
class UserNotConnectedError(MusicError):
|
||||
def __init__(self, message: str, inner_error: Optional[Exception] = None):
|
||||
super().__init__(message, inner_error=inner_error)
|
||||
|
||||
|
||||
class PlayBackStatusError(MusicError):
|
||||
def __init__(self, message: str, inner_error: Optional[Exception] = None):
|
||||
super().__init__(message, inner_error=inner_error)
|
||||
|
||||
|
||||
class RequirePlayingError(PlayBackStatusError):
|
||||
def __init__(self, message: str, inner_error: Optional[Exception] = None):
|
||||
super().__init__(message, inner_error=inner_error)
|
||||
|
||||
|
||||
class RequireListeningError(PlayBackStatusError):
|
||||
def __init__(self, message: str, inner_error: Optional[Exception] = None):
|
||||
super().__init__(message, inner_error=inner_error)
|
||||
|
||||
|
||||
# Channel errors
|
||||
class ChannelError(MusicError):
|
||||
def __init__(self, message: str, channels, inner_error: Optional[Exception] = None):
|
||||
self.channels = channels
|
||||
super().__init__()
|
||||
super().__init__(message=message, inner_error=inner_error)
|
||||
|
||||
|
||||
class WrongVoiceChannelError(WrongChannelError):
|
||||
def __init__(self, e: str, channels=None):
|
||||
super().__init__(e, channels=channels)
|
||||
class VoiceChannelFullError(ChannelError):
|
||||
def __init__(self, message: str, channel, inner_error: Optional[Exception] = None):
|
||||
super().__init__(message, channels=[channel], inner_error=inner_error)
|
||||
|
||||
|
||||
class WrongTextChannelError(WrongChannelError):
|
||||
def __init__(self, e: str, channels=None):
|
||||
super().__init__(e, channels=channels)
|
||||
class UserInDifferentVoiceChannelError(ChannelError):
|
||||
def __init__(self, message: str, channel, inner_error: Optional[Exception] = None):
|
||||
super().__init__(message, channels=[channel], inner_error=inner_error)
|
||||
|
||||
|
||||
class ConfiguredChannelsError(ChannelError):
|
||||
def __init__(self, message: str, channels, inner_error: Optional[Exception] = None):
|
||||
super().__init__(message, channels=channels, inner_error=inner_error)
|
||||
|
||||
|
||||
class WrongVoiceChannelError(ConfiguredChannelsError):
|
||||
def __init__(self, message: str, channels, inner_error: Optional[Exception] = None):
|
||||
super().__init__(message, channels=channels, inner_error=inner_error)
|
||||
|
||||
|
||||
class WrongTextChannelError(ConfiguredChannelsError):
|
||||
def __init__(self, message: str, channels, inner_error: Optional[Exception] = None):
|
||||
super().__init__(message, channels=channels, inner_error=inner_error)
|
||||
@@ -0,0 +1,41 @@
|
||||
# Discord Packages
|
||||
import discord
|
||||
|
||||
from bot import MusicBot
|
||||
from musicbot.cogs.music.music_errors import MusicError
|
||||
|
||||
|
||||
class BasicVoiceClient(discord.VoiceClient):
|
||||
def __init__(self, client: MusicBot, channel: discord.VoiceChannel):
|
||||
# Needs to be named client in order for base class to work
|
||||
# in most cases lavalink handles disconnects, but if we force it then we'll get an error.
|
||||
# during self.cleanup()
|
||||
self.client = client
|
||||
self.channel = channel
|
||||
if self.client.lavalink:
|
||||
self.lavalink = self.client.lavalink
|
||||
else:
|
||||
raise MusicError("client did not have defined lavalink before connect")
|
||||
|
||||
async def on_voice_server_update(self, data):
|
||||
await self.lavalink.voice_update_handler({'t': 'VOICE_SERVER_UPDATE', 'd': data})
|
||||
|
||||
async def on_voice_state_update(self, data):
|
||||
await self.lavalink.voice_update_handler({'t': 'VOICE_STATE_UPDATE', 'd': data})
|
||||
|
||||
async def connect(self, *, timeout: float, reconnect: bool, self_deaf: bool = False,
|
||||
self_mute: bool = False) -> None:
|
||||
await self.channel.guild.change_voice_state(channel=self.channel, self_mute=self_mute, self_deaf=self_deaf)
|
||||
|
||||
async def disconnect(self, *, force: bool = False) -> None:
|
||||
player = self.lavalink.player_manager.get(self.channel.guild.id)
|
||||
|
||||
if player:
|
||||
# no need to disconnect if we are not connected
|
||||
if not force and not player.is_connected:
|
||||
return
|
||||
|
||||
# None means disconnect
|
||||
await self.channel.guild.change_voice_state(channel=None)
|
||||
player.channel_id = None
|
||||
self.cleanup()
|
||||
@@ -1,21 +1,27 @@
|
||||
# Discord Packages
|
||||
import discord
|
||||
import lavalink
|
||||
from discord.ext import commands
|
||||
from lavalink import Client, Node
|
||||
|
||||
import codecs
|
||||
from typing import Optional
|
||||
|
||||
import yaml
|
||||
|
||||
from bot import MusicBot
|
||||
from musicbot.cogs.music.music_errors import MusicError
|
||||
|
||||
# Bot Utilities
|
||||
from musicbot.utils.settingsmanager import Settings
|
||||
from ..utils.mixplayer import MixPlayer
|
||||
from ..utils.userinteraction import ClearOn, Scroller
|
||||
from .helpformatter import commandhelper
|
||||
|
||||
|
||||
class NodeManager(commands.Cog):
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
self.settings = self.bot.settings
|
||||
def __init__(self, bot: MusicBot):
|
||||
self.bot: MusicBot = bot
|
||||
self.settings: Settings = self.bot.settings
|
||||
self.logger = self.bot.main_logger.bot_logger.getChild("NodeManager")
|
||||
|
||||
async def load_music_cogs(self):
|
||||
@@ -23,8 +29,9 @@ class NodeManager(commands.Cog):
|
||||
'musicbot.cogs.music',
|
||||
]
|
||||
|
||||
if not hasattr(self.bot, 'lavalink'):
|
||||
self.bot.lavalink = Client(self.bot.user.id, player=MixPlayer)
|
||||
if self.bot.lavalink is None and self.bot.user:
|
||||
self.bot.lavalink = lavalink.Client(self.bot.user.id, player=MixPlayer)
|
||||
self.lavalink = self.bot.lavalink
|
||||
|
||||
self.load_nodes_from_file()
|
||||
|
||||
@@ -36,6 +43,9 @@ class NodeManager(commands.Cog):
|
||||
await self.bot.load_extension(extension)
|
||||
except Exception:
|
||||
self.logger.exception("Loading of extension %s failed" % extension)
|
||||
else:
|
||||
self.logger.error("Failed to initialize lavalink client")
|
||||
raise MusicError("Failed to initialize lavalink client")
|
||||
|
||||
def load_nodes_from_file(self):
|
||||
with codecs.open(f"{self.bot.datadir}/config.yaml", 'r', encoding='utf8') as f:
|
||||
@@ -44,19 +54,20 @@ class NodeManager(commands.Cog):
|
||||
name_cache = []
|
||||
new_nodes = []
|
||||
|
||||
for node in self.bot.lavalink.node_manager.nodes:
|
||||
for node in self.lavalink.node_manager.nodes:
|
||||
name_cache.append(node.name)
|
||||
|
||||
for node in conf['lavalink nodes'] + self.settings.get('lavalink', 'nodes', []):
|
||||
if node['name'] in name_cache:
|
||||
continue
|
||||
name_cache.append(node['name'])
|
||||
|
||||
self.bot.lavalink.add_node(**node)
|
||||
self.lavalink.add_node(**node)
|
||||
|
||||
self.logger.debug("Adding Lavalink node: %s on %s with the port %s in %s" % (
|
||||
node['name'], node['host'],
|
||||
node['port'], node['region'],))
|
||||
new_nodes.append({**node})
|
||||
name_cache.append(node['name'])
|
||||
return new_nodes
|
||||
|
||||
async def _regioner(self, region):
|
||||
@@ -89,7 +100,7 @@ class NodeManager(commands.Cog):
|
||||
embed.add_field(name=f'{await self._regioner(n.region)} **Name:** {n.name}',
|
||||
value=f'**Host:** {n.host}\n **Port:** {n.port}')
|
||||
|
||||
if isinstance(node, Node):
|
||||
if isinstance(node, lavalink.Node):
|
||||
embed.add_field(name=f'{await self._regioner(node.region)} **Name:** {node.name}',
|
||||
value=f'**Host:** {node.host}\n **Port:** {node.port}')
|
||||
|
||||
@@ -127,24 +138,24 @@ class NodeManager(commands.Cog):
|
||||
|
||||
@_node.command(name='add')
|
||||
@commands.is_owner()
|
||||
async def _add(self, ctx, host, port, password, region, name=None):
|
||||
if name in [n.name for n in self.bot.lavalink.node_manager.nodes]:
|
||||
async def _add(self, ctx, host: str, port: int, password: str, region, name: Optional[str] = None):
|
||||
if name in [n.name for n in self.lavalink.node_manager.nodes]:
|
||||
return await ctx.send("A node with that name already exists")
|
||||
|
||||
self.bot.lavalink.add_node(host, port, password, region, name=name)
|
||||
self.lavalink.add_node(host, port, password, region, name=name)
|
||||
self.logger.debug("Adding Lavalink node: %s on %s with the port %s in %s" % (host, port, region, name,))
|
||||
embed = await self._node_presenter(ctx, {'host': host, 'port': port, 'password': password,
|
||||
'region': region, 'name': name})
|
||||
embed.title = 'Added new node!'
|
||||
|
||||
self.settings.set('lavalink', 'nodes', [self.get_node_properties(n) for
|
||||
n in self.bot.lavalink.node_manager.nodes])
|
||||
n in self.lavalink.node_manager.nodes])
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@_node.command(name='list')
|
||||
@commands.is_owner()
|
||||
async def list_nodes(self, ctx):
|
||||
embed = await self._node_presenter(ctx, self.bot.lavalink.node_manager.nodes)
|
||||
embed = await self._node_presenter(ctx, self.lavalink.node_manager.nodes)
|
||||
embed.title = 'Lavalink nodes attatched to this bot:'
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@@ -152,8 +163,8 @@ class NodeManager(commands.Cog):
|
||||
@commands.is_owner()
|
||||
async def _remove(self, ctx, node):
|
||||
sent_feedback = False
|
||||
for _node in self.bot.lavalink.node_manager.nodes:
|
||||
if len(self.bot.lavalink.node_manager.nodes) <= 1:
|
||||
for _node in self.lavalink.node_manager.nodes:
|
||||
if len(self.lavalink.node_manager.nodes) <= 1:
|
||||
await ctx.send('Cannot remove the last node')
|
||||
sent_feedback = True
|
||||
break
|
||||
@@ -162,11 +173,11 @@ class NodeManager(commands.Cog):
|
||||
embed = await self._node_presenter(ctx, _node)
|
||||
embed.title = 'Removed node from bot'
|
||||
await ctx.send(embed=embed)
|
||||
self.bot.lavalink.node_manager.remove_node(_node)
|
||||
self.logger.info("Removed Lavalink node: %s" % _node.host)
|
||||
self.lavalink.node_manager.remove_node(_node)
|
||||
self.logger.info("Removed Lavalink node: %s, %s" % (_node.name, _node.host))
|
||||
|
||||
self.settings.set('lavalink', 'nodes', [self.get_node_properties(n) for
|
||||
n in self.bot.lavalink.node_manager.nodes])
|
||||
n in self.lavalink.node_manager.nodes])
|
||||
|
||||
if not sent_feedback:
|
||||
await ctx.send('No node found')
|
||||
@@ -174,7 +185,7 @@ class NodeManager(commands.Cog):
|
||||
@_node.command(name='change')
|
||||
@commands.is_owner()
|
||||
async def _nodechange(self, ctx, node=None):
|
||||
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
player = self.lavalink.player_manager.get(ctx.guild.id)
|
||||
|
||||
if not player:
|
||||
return
|
||||
@@ -185,7 +196,7 @@ class NodeManager(commands.Cog):
|
||||
|
||||
else:
|
||||
newnode = None
|
||||
for _node in self.bot.lavalink.node_manager.nodes:
|
||||
for _node in self.lavalink.node_manager.nodes:
|
||||
if (_node.name or _node.host) == node:
|
||||
newnode = _node
|
||||
|
||||
|
||||
+17
-12
@@ -2,15 +2,20 @@
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from bot import MusicBot
|
||||
|
||||
from ..utils import checks
|
||||
from ..utils.settingsmanager import Settings as SettingsManager
|
||||
from ..utils.userinteraction import ClearOn, Scroller
|
||||
from .helpformatter import commandhelper
|
||||
|
||||
|
||||
class Settings(commands.Cog):
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
self.settings = self.bot.settings
|
||||
def __init__(self, bot: MusicBot):
|
||||
self.bot: MusicBot = bot
|
||||
self.settings: SettingsManager = self.bot.settings
|
||||
|
||||
def format_prefixes(self, prefixes):
|
||||
if prefixes is None:
|
||||
@@ -90,8 +95,8 @@ class Settings(commands.Cog):
|
||||
|
||||
if textchannels := self.bot.settings.get(ctx.guild, 'channels.text'):
|
||||
embed = discord.Embed(title='Music command channels set', color=ctx.me.color)
|
||||
channels = [ctx.guild.get_channel(channel) for channel in textchannels]
|
||||
mentioned = [channel.mention for channel in channels if channel is not None]
|
||||
configured_channels = [ctx.guild.get_channel(channel) for channel in textchannels]
|
||||
mentioned = [channel.mention for channel in configured_channels if channel is not None]
|
||||
embed.description = ', '.join(mentioned)
|
||||
await ctx.send(embed=embed)
|
||||
else:
|
||||
@@ -110,8 +115,8 @@ class Settings(commands.Cog):
|
||||
|
||||
if musicchannels := self.bot.settings.get(ctx.guild, 'channels.music'):
|
||||
embed = discord.Embed(title='Music channels set', color=ctx.me.color)
|
||||
channels = [ctx.guild.get_channel(channel) for channel in musicchannels]
|
||||
mentioned = [channel.name for channel in channels if channel is not None]
|
||||
configured_channels = [ctx.guild.get_channel(channel) for channel in musicchannels]
|
||||
mentioned = [channel.name for channel in configured_channels if channel is not None]
|
||||
embed.description = ', '.join(mentioned)
|
||||
await ctx.send(embed=embed)
|
||||
else:
|
||||
@@ -130,8 +135,8 @@ class Settings(commands.Cog):
|
||||
|
||||
if listenchannels := self.bot.settings.get(ctx.guild, 'channels.listen_only'):
|
||||
embed = discord.Embed(title='Listen only channels set', color=ctx.me.color)
|
||||
channels = [ctx.guild.get_channel(channel) for channel in listenchannels]
|
||||
mentioned = [channel.name for channel in channels if channel is not None]
|
||||
configured_channels = [ctx.guild.get_channel(channel) for channel in listenchannels]
|
||||
mentioned = [channel.name for channel in configured_channels if channel is not None]
|
||||
embed.description = ', '.join(mentioned)
|
||||
await ctx.send(embed=embed)
|
||||
else:
|
||||
@@ -150,8 +155,8 @@ class Settings(commands.Cog):
|
||||
|
||||
if djroles := self.bot.settings.get(ctx.guild, 'roles.dj'):
|
||||
embed = discord.Embed(title='DJ roles set to', color=ctx.me.color)
|
||||
roles = [ctx.guild.get_role(role) for role in djroles]
|
||||
mentioned = [role.name for role in roles if role is not None]
|
||||
configured_roles = [ctx.guild.get_role(role) for role in djroles]
|
||||
mentioned = [role.name for role in configured_roles if role is not None]
|
||||
embed.description = ', '.join(mentioned)
|
||||
await ctx.send(embed=embed)
|
||||
else:
|
||||
@@ -161,7 +166,7 @@ class Settings(commands.Cog):
|
||||
@checks.is_admin()
|
||||
@commands.guild_only()
|
||||
@_set.command(name='maxduration')
|
||||
async def set_max_track_duration(self, ctx, duration: int = None):
|
||||
async def set_max_track_duration(self, ctx, duration: Optional[int] = None):
|
||||
self.bot.settings.set(ctx.guild, 'duration.max', duration)
|
||||
|
||||
if duration := self.bot.settings.get(ctx.guild, 'duration.max'):
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
# Discord Packages
|
||||
from discord.ext import commands
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from .localizer.localizerwrapper import LocalizerWrapper
|
||||
|
||||
|
||||
class LocalizedContext(commands.Context):
|
||||
def __init__(self, **kwargs):
|
||||
@@ -9,3 +13,5 @@ class LocalizedContext(commands.Context):
|
||||
self.locale = self.bot.settings.get(self.message.guild, 'locale', 'default_locale')
|
||||
else:
|
||||
self.locale = self.bot.settings.default_locale
|
||||
|
||||
self.localizer: Optional[LocalizerWrapper] = None
|
||||
@@ -54,8 +54,8 @@ class BotLogger(object):
|
||||
self.bot_logger.info(f"Creating logging directory: {self.log_location}")
|
||||
if self.log_level:
|
||||
self.bot_logger.info(f"Logging directory: {self.log_location}")
|
||||
file_logger = logging.handlers.RotatingFileHandler(f'{self.log_location}/bot.log', mode='a',
|
||||
maxBytes=5000, encoding="UTF-8", delay=0, backupCount=5)
|
||||
file_logger = logging.handlers.TimedRotatingFileHandler(f'{self.log_location}/bot.log',
|
||||
when="midnight", encoding="UTF-8", backupCount=5)
|
||||
file_logger.setLevel(self.log_level)
|
||||
file_logger.setFormatter(file_format)
|
||||
self.bot_logger.addHandler(file_logger)
|
||||
@@ -1,3 +1,6 @@
|
||||
# Discord Packages
|
||||
from lavalink.models import AudioTrack
|
||||
|
||||
import logging
|
||||
from collections import OrderedDict, deque
|
||||
from itertools import chain, cycle, islice
|
||||
@@ -5,7 +8,7 @@ from random import shuffle
|
||||
from typing import Generic, Iterable, Iterator, List, Optional, Tuple, TypeVar
|
||||
|
||||
# Would like to ensure the T has a "requester" attribute, but don't know if that is possible
|
||||
T = TypeVar('T')
|
||||
T = TypeVar('T', bound=AudioTrack)
|
||||
QueueType = List[T]
|
||||
|
||||
|
||||
@@ -31,7 +34,6 @@ class MixQueue(Generic[T]):
|
||||
self.looping = False
|
||||
self.loop_offset = 0
|
||||
self.logger = logging.getLogger("musicbot").getChild("Queue")
|
||||
self.logger = self.logger.getChild("Queue")
|
||||
|
||||
def __str__(self) -> str:
|
||||
tmp = ''
|
||||
@@ -96,9 +98,9 @@ class MixQueue(Generic[T]):
|
||||
self._history.append(next_track)
|
||||
return next_track
|
||||
except IndexError as e:
|
||||
self.logger.error(e)
|
||||
self.logger.error(self.loop_offset)
|
||||
self.logger.error(len(self))
|
||||
self.logger.error(f"Failed to get the next track in looping mode: loop offset {self.loop_offset}." +
|
||||
" Queue length: {len(self)}")
|
||||
self.logger.exception(e)
|
||||
self.loop_offset = min(self.loop_offset, len(self)-1)
|
||||
else:
|
||||
if first_queue := self.first_queue:
|
||||
@@ -123,12 +125,13 @@ class MixQueue(Generic[T]):
|
||||
localpos = pos
|
||||
|
||||
# Return info about track position
|
||||
return track, self._loc_to_glob(requester, localpos), localpos
|
||||
global_position = self._loc_to_glob(requester, localpos)
|
||||
return track, global_position, localpos
|
||||
|
||||
def add_next_track(self, track: T) -> None:
|
||||
def add_priorty_queue_track(self, track: T) -> None:
|
||||
self.priority_queue.append(track)
|
||||
|
||||
def remove_user_queue(self, requester: int) -> None:
|
||||
def remove_user_queue(self, requester: int) -> QueueType:
|
||||
user_queue = self.queues.get(requester, [])
|
||||
if user_queue:
|
||||
if self.looping:
|
||||
@@ -137,6 +140,7 @@ class MixQueue(Generic[T]):
|
||||
self.remove_user_track(requester, pos)
|
||||
else:
|
||||
self.queues.pop(requester)
|
||||
return user_queue
|
||||
|
||||
def remove_user_track(self, requester: int, pos: int) -> Optional[T]:
|
||||
user_queue = self.queues.get(requester)
|
||||
@@ -149,6 +153,19 @@ class MixQueue(Generic[T]):
|
||||
self._clear_empty()
|
||||
return track
|
||||
|
||||
def remove_track(self, track: T) -> Optional[Tuple[int, T]]:
|
||||
"""
|
||||
Removes a track by identity
|
||||
"""
|
||||
for queue in self.queues.values():
|
||||
for i, t in enumerate(queue):
|
||||
if t is track:
|
||||
global_pos = self._loc_to_glob(t.requester, i)
|
||||
removed = queue.pop(i)
|
||||
self._clear_empty()
|
||||
return global_pos, removed
|
||||
return None
|
||||
|
||||
def remove_global_track(self, pos: int) -> Optional[T]:
|
||||
# Get the actual index of the song
|
||||
index = list(self).index(self.get_queue()[pos])
|
||||
@@ -172,6 +189,7 @@ class MixQueue(Generic[T]):
|
||||
queue.insert(final, track)
|
||||
return track
|
||||
except IndexError:
|
||||
self.logger.debug(f"Got invalid index when moving track from {initial} to {final}")
|
||||
pass
|
||||
|
||||
def shuffle_user_queue(self, requester: int) -> None:
|
||||
@@ -180,17 +198,19 @@ class MixQueue(Generic[T]):
|
||||
shuffle(queue)
|
||||
|
||||
def enable_looping(self, looping: bool) -> None:
|
||||
if (not self.looping) and looping: # Not enabled and turning on
|
||||
if (not self.looping) and looping: # Enable only if not already enabled
|
||||
self.looping = looping
|
||||
self.loop_offset = 0
|
||||
elif self.looping and not looping: # Enabled to turning off
|
||||
tmp = [track for track in self.get_queue()]
|
||||
for key, _ in self.queues.items():
|
||||
self.queues = OrderedDict()
|
||||
self.logger.debug("Looping enabled")
|
||||
elif self.looping and not looping: # Disable only if enabled
|
||||
# Copy queue in current (looping) state
|
||||
queue_copy = [track for track in self.get_queue()]
|
||||
self.queues = OrderedDict()
|
||||
self.looping = looping
|
||||
for track in tmp:
|
||||
for track in queue_copy:
|
||||
self.add_track(track.requester, track)
|
||||
self.loop_offset = 0
|
||||
self.logger.debug("Looping disabled, queue reordered")
|
||||
|
||||
# Switches the order of user queues
|
||||
def _shift_queues(self) -> None:
|
||||
@@ -217,9 +237,9 @@ class MixQueue(Generic[T]):
|
||||
globpos += 1
|
||||
return globpos
|
||||
|
||||
def _glob_to_loc(self, pos: int) -> Tuple[Optional[T], Optional[int]]:
|
||||
def _glob_to_loc(self, pos: int) -> Tuple[Optional[int], Optional[int]]:
|
||||
try:
|
||||
song = next(islice(self, pos, pos + 1))
|
||||
song: T = next(islice(self, pos, pos + 1))
|
||||
except (ValueError, StopIteration):
|
||||
return None, None
|
||||
user_queue = self.queues.get(song.requester, [])
|
||||
|
||||
@@ -5,10 +5,13 @@ from lavalink import AudioTrack, DefaultPlayer, Node
|
||||
from lavalink.events import QueueEndEvent, TrackEndEvent, TrackExceptionEvent, TrackStartEvent, TrackStuckEvent
|
||||
from lavalink.filters import Equalizer, Timescale
|
||||
|
||||
import logging
|
||||
from typing import Dict, List, Optional, Set, Tuple
|
||||
|
||||
from .mixqueue import MixQueue
|
||||
|
||||
RequesterType = discord.Member
|
||||
|
||||
|
||||
class MixPlayer(DefaultPlayer):
|
||||
def __init__(self, guild_id: int, node: Node):
|
||||
@@ -16,11 +19,12 @@ class MixPlayer(DefaultPlayer):
|
||||
|
||||
self.queue: MixQueue[AudioTrack] = MixQueue()
|
||||
|
||||
self.listeners: Set[int] = set()
|
||||
self.voteables: Dict[int, Set[int]] = {}
|
||||
self.listeners: Set[RequesterType] = set()
|
||||
self.voteables: Dict[int, Set[RequesterType]] = {}
|
||||
self.skip_voters: Set[int] = set()
|
||||
self.boosted: bool = False
|
||||
self.nightcore_enabled: bool = False
|
||||
self.logger = logging.getLogger("musicbot").getChild("MixPlayer")
|
||||
bands = [
|
||||
(0, 0.15),
|
||||
(1, 0.15),
|
||||
@@ -36,38 +40,53 @@ class MixPlayer(DefaultPlayer):
|
||||
self.nightcore_filter: lavalink.Filter = Timescale()
|
||||
self.nightcore_filter.update(speed=1.25, pitch=1.25)
|
||||
|
||||
def add(self, requester: int, track: AudioTrack, pos: Optional[int] = None) -> Tuple[AudioTrack, int, int]:
|
||||
def add(self, requester: RequesterType, track: AudioTrack,
|
||||
pos: Optional[int] = None) -> Tuple[AudioTrack, int, int]:
|
||||
""" Adds a track to the queue. """
|
||||
return self.queue.add_track(requester, track, pos)
|
||||
track, global_position, localpos = self.queue.add_track(requester.id, track, pos)
|
||||
self.logger.info(f"Track {track.title} added for {requester.display_name} @ ({global_position}, {localpos}).")
|
||||
return track, global_position, localpos
|
||||
|
||||
def add_priority(self, track: AudioTrack):
|
||||
""" Adds a track to beginning of the queue """
|
||||
self.queue.add_next_track(track)
|
||||
self.queue.add_priorty_queue_track(track)
|
||||
self.logger.info(f"Track {track} added to priority queue.")
|
||||
|
||||
def move_user_track(self, requester: int, initial: int, final: int):
|
||||
def move_user_track(self, requester: RequesterType, initial: int, final: int):
|
||||
""" Moves a track in a users queue"""
|
||||
return self.queue.move_user_track(requester, initial, final)
|
||||
if moved := self.queue.move_user_track(requester.id, initial, final):
|
||||
self.logger.info(f"Track {moved.title} moved from {initial} to {final}" +
|
||||
f"for in queue for {requester.display_name}")
|
||||
|
||||
def remove_user_queue(self, requester: int):
|
||||
self.queue.remove_user_queue(requester)
|
||||
def remove_user_queue(self, requester: RequesterType):
|
||||
removed = self.queue.remove_user_queue(requester.id)
|
||||
if removed: # Removed queues can be empty
|
||||
self.logger.info(f"Queue for requester {requester.display_name} removed. {len(removed)} tracks removed")
|
||||
else:
|
||||
self.logger.debug(f"User {requester.display_name} has no more tracks. Remove from queue")
|
||||
|
||||
def remove_user_track(self, requester: int, pos: int) -> Optional[AudioTrack]:
|
||||
def remove_user_track(self, requester: RequesterType, pos: int) -> Optional[AudioTrack]:
|
||||
""" Removes the song at <pos> from the queue of requester """
|
||||
return self.queue.remove_user_track(requester, pos)
|
||||
if track := self.queue.remove_user_track(requester.id, pos):
|
||||
self.logger.info(f"Track {track.title} for requester {requester.display_name} removed.")
|
||||
return track
|
||||
|
||||
def remove_track(self, track: AudioTrack) -> Optional[Tuple[int, AudioTrack]]:
|
||||
return self.queue.remove_track(track)
|
||||
|
||||
def remove_global_track(self, pos: int) -> Optional[AudioTrack]:
|
||||
""" Removes the song at <pos> in the global queue """
|
||||
return self.queue.remove_global_track(pos)
|
||||
|
||||
def shuffle_user_queue(self, requester: int):
|
||||
def shuffle_user_queue(self, requester: RequesterType):
|
||||
""" Randomly reorders the queue of requester """
|
||||
self.queue.shuffle_user_queue(requester)
|
||||
self.queue.shuffle_user_queue(requester.id)
|
||||
|
||||
def user_queue(self, user: int) -> List[AudioTrack]:
|
||||
return self.queue.get_user_queue(user)
|
||||
def user_queue(self, requester: RequesterType) -> List[AudioTrack]:
|
||||
return self.queue.get_user_queue(requester.id)
|
||||
|
||||
def user_queue_with_global_index(self, user: int) -> List[Tuple[AudioTrack, int]]:
|
||||
return self.queue.get_user_queue_with_index(user)
|
||||
def user_queue_with_global_index(self, requester: RequesterType) -> List[Tuple[AudioTrack, int]]:
|
||||
return self.queue.get_user_queue_with_index(requester.id)
|
||||
|
||||
def global_queue(self) -> List[AudioTrack]:
|
||||
return self.queue.get_queue()
|
||||
@@ -76,9 +95,9 @@ class MixPlayer(DefaultPlayer):
|
||||
return self.queue.history
|
||||
|
||||
def queue_duration(self, include_current: bool = False,
|
||||
member: Optional[discord.Member] = None, end_pos: Optional[int] = None):
|
||||
member: Optional[RequesterType] = None, end_pos: Optional[int] = None):
|
||||
duration = 0
|
||||
queue = self.user_queue(member.id) if member else self.queue
|
||||
queue = self.user_queue(member) if member else self.queue
|
||||
|
||||
for i, track in enumerate(queue):
|
||||
if i == end_pos:
|
||||
@@ -101,8 +120,9 @@ class MixPlayer(DefaultPlayer):
|
||||
|
||||
if not track:
|
||||
if self.queue.empty:
|
||||
self.boosted = False
|
||||
self.nightcore_enabled = False
|
||||
self.logger.debug("No track provided to play function and queue is empty. Resetting Audio filters")
|
||||
await self.bassboost(False)
|
||||
await self.nightcoreify(False)
|
||||
await self.stop()
|
||||
await self.node._dispatch_event(QueueEndEvent(self))
|
||||
return
|
||||
@@ -117,11 +137,14 @@ class MixPlayer(DefaultPlayer):
|
||||
await self.node._send(op='play', guildId=str(self.guild_id),
|
||||
track=track.track, startTime=start_time)
|
||||
await self.node._dispatch_event(TrackStartEvent(self, track))
|
||||
self.logger.info(f"Playing track: {track.title}")
|
||||
|
||||
async def skip(self, pos: int = 0):
|
||||
""" Plays the next track in the queue, if any. """
|
||||
for i in range(pos):
|
||||
_ = self.queue.pop_first()
|
||||
for _ in range(pos):
|
||||
track = self.queue.pop_first()
|
||||
self.logger.debug(f"Track {track} skipped")
|
||||
self.logger.info(f"Skipped {pos + 1} tracks")
|
||||
self.clear_votes()
|
||||
await self.play()
|
||||
|
||||
@@ -129,36 +152,43 @@ class MixPlayer(DefaultPlayer):
|
||||
""" Stops the player. """
|
||||
await self.node._send(op='stop', guildId=str(self.guild_id))
|
||||
self.current = None
|
||||
self.queue.looping = False
|
||||
self.queue.enable_looping(False)
|
||||
self.logger.info("Music player stopped, clearing current track and stopping looping")
|
||||
self.clear_votes()
|
||||
|
||||
def update_listeners(self, member, voice_state):
|
||||
def update_listeners(self, member: RequesterType, voice_state):
|
||||
if self.channel_id is not None:
|
||||
vc = int(self.channel_id)
|
||||
if voice_state.channel is None or voice_state.channel.id != vc:
|
||||
self.logger.debug(f"User {member.display_name} left the voice channel")
|
||||
self.listeners.discard(member)
|
||||
self.remove_member_votes(member)
|
||||
else:
|
||||
if voice_state.deaf or voice_state.self_deaf:
|
||||
self.logger.debug(f"User {member.display_name} is now deafened")
|
||||
self.listeners.discard(member)
|
||||
self.remove_member_votes(member)
|
||||
else:
|
||||
self.listeners.add(member)
|
||||
self.logger.debug(f"Member voice state update. Votes are now {self.voteables}")
|
||||
|
||||
def clear_listeners(self):
|
||||
self.logger.debug("Listeners cleared")
|
||||
self.listeners.clear()
|
||||
|
||||
def add_vote(self, category, member):
|
||||
def add_vote(self, category, member: RequesterType):
|
||||
if member in self.listeners:
|
||||
if category not in self.voteables:
|
||||
self.voteables[category] = set()
|
||||
self.voteables[category].add(member)
|
||||
self.logger.debug(f"{member.display_name} added vote for {category}")
|
||||
|
||||
def remove_member_votes(self, member):
|
||||
def remove_member_votes(self, member: RequesterType):
|
||||
for _, votes in self.voteables.items():
|
||||
votes.discard(member)
|
||||
self.logger.debug(f"Removing votes for {member.display_name}.")
|
||||
|
||||
def get_voters(self, category):
|
||||
def get_voters(self, category) -> Set[RequesterType]:
|
||||
if category not in self.voteables:
|
||||
self.voteables[category] = set()
|
||||
return self.voteables[category]
|
||||
@@ -167,7 +197,7 @@ class MixPlayer(DefaultPlayer):
|
||||
for _, votes in self.voteables.items():
|
||||
votes.clear()
|
||||
|
||||
def enable_looping(self, looping):
|
||||
def enable_looping(self, looping: bool):
|
||||
if (not self.queue.looping) and looping:
|
||||
self.queue.enable_looping(looping)
|
||||
if track := self.current:
|
||||
@@ -180,23 +210,27 @@ class MixPlayer(DefaultPlayer):
|
||||
""" Handles the given event as necessary. """
|
||||
if isinstance(event, (TrackStuckEvent, TrackExceptionEvent)) or \
|
||||
isinstance(event, TrackEndEvent) and event.reason == 'FINISHED':
|
||||
self.logger.debug("Track ended, clearing votes")
|
||||
self.skip_voters.clear()
|
||||
for _, votes in self.voteables.items():
|
||||
votes.clear()
|
||||
await self.play()
|
||||
|
||||
async def bassboost(self, boost: bool = False):
|
||||
async def bassboost(self, boost: bool):
|
||||
self.boosted = boost
|
||||
if boost:
|
||||
await self.set_filter(self.bass_boost_filter)
|
||||
else:
|
||||
self.logger.info("Disabling bass boost")
|
||||
await self.remove_filter(self.bass_boost_filter)
|
||||
|
||||
async def nightcoreify(self, nightcore: bool = False):
|
||||
async def nightcoreify(self, nightcore: bool):
|
||||
self.nightcore_enabled = nightcore
|
||||
if nightcore:
|
||||
self.logger.info("Enabling nightcore mode")
|
||||
await self.set_filter(self.nightcore_filter)
|
||||
else:
|
||||
self.logger.info("Disabling nightcore mode")
|
||||
await self.remove_filter(self.nightcore_filter)
|
||||
|
||||
@property
|
||||
|
||||
@@ -62,8 +62,8 @@ class TestMixQueue():
|
||||
def test_priority_queue(self):
|
||||
queue = self.setup_baisc_queue()
|
||||
|
||||
queue.add_next_track("priority_item1")
|
||||
queue.add_next_track("priority_item2")
|
||||
queue.add_priorty_queue_track("priority_item1")
|
||||
queue.add_priorty_queue_track("priority_item2")
|
||||
|
||||
assert queue.pop_first() == "priority_item1"
|
||||
assert queue.pop_first() == "priority_item2"
|
||||
@@ -121,3 +121,28 @@ class TestMixQueue():
|
||||
assert item == queue.pop_first()
|
||||
|
||||
assert list(queue) == []
|
||||
|
||||
def test_remove_by_identity(self):
|
||||
queue = MixQueue()
|
||||
queue.add_track(1, TrackMock(1, "a"))
|
||||
queue.add_track(1, TrackMock(1, "a"))
|
||||
queue.add_track(1, TrackMock(1, "b"))
|
||||
queue.add_track(1, TrackMock(1, "a"))
|
||||
queue.add_track(1, TrackMock(1, "b"))
|
||||
queue.add_track(1, TrackMock(1, "a"))
|
||||
queue.add_track(1, TrackMock(1, "b"))
|
||||
queue.add_track(1, TrackMock(1, "b"))
|
||||
|
||||
assert queue.remove_track(TrackMock(1, "a")) is None
|
||||
to_remove = queue.get_user_queue(1)[3]
|
||||
|
||||
first_remove = queue.remove_track(to_remove)
|
||||
assert first_remove is not None
|
||||
pos, removed = first_remove
|
||||
assert to_remove is removed
|
||||
assert pos == 3
|
||||
assert queue.remove_track(to_remove) is None
|
||||
|
||||
queue_after_id_remove = self.list_to_requests(["1a", "1a", "1b", "1b", "1a", "1b", "1b"])
|
||||
for item in queue_after_id_remove:
|
||||
assert item == queue.pop_first()
|
||||
@@ -4,6 +4,7 @@ import discord
|
||||
import codecs
|
||||
import locale as localee
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
|
||||
@@ -53,7 +54,7 @@ class Settings():
|
||||
with codecs.open(self._SETTINGS_PATH, 'w', encoding='utf8') as f:
|
||||
yaml.dump(self.settings, f, indent=2)
|
||||
|
||||
def get(self, identifier, setting, default=''):
|
||||
def get(self, identifier, setting, default: Any = '') -> Any:
|
||||
""" Gets a value from the settings if a default return value is specified
|
||||
it will return the default if no setting is found. If that default is an
|
||||
attribute of the settings class, the value of the attribute will get returned."""
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
from .dictmapper import DictMapper
|
||||
|
||||
|
||||
class TestDictMapper():
|
||||
def test_dict_mapper_basic_set(self):
|
||||
d = {}
|
||||
DictMapper.set(d, ["a", "b", "c"], 1)
|
||||
assert d == {"a": {"b": {"c": 1}}}
|
||||
|
||||
def test_dict_mapper_basic_get(self):
|
||||
d = {"a": {"b": {"c": 1}}}
|
||||
assert DictMapper.get(d, ["a", "b", "c"]) == 1
|
||||
|
||||
def test_get_variations(self):
|
||||
d = {"a": {"b": {"c": 1}}}
|
||||
|
||||
assert DictMapper.get(d, ["a", "b", "c"]) == DictMapper.get(d["a"], ["b", "c"])
|
||||
assert DictMapper.get(d, ["a", "b", "c"]) == DictMapper.get(d["a"]["b"], ["c"])
|
||||
assert DictMapper.get(d, ["a", "b", "c"]) == d["a"]["b"]["c"]
|
||||
|
||||
def test_get_sub_dictionaries(self):
|
||||
d = {"a": {"b": {"c": 1}}}
|
||||
assert DictMapper.get(d, ["a", "b"]) == d["a"]["b"]
|
||||
assert DictMapper.get(d["a"], ["b"]) == DictMapper.get(d["a"], ["b"])
|
||||
|
||||
def test_set_variation(self):
|
||||
d = {"a": {"b": {"c": 1}}}
|
||||
d1 = {}
|
||||
d2 = {}
|
||||
|
||||
DictMapper.set(d1, ["a", "b"], {"c": 1})
|
||||
assert d1 == d
|
||||
|
||||
DictMapper.set(d2, ["a"], {"b": {"c": 1}})
|
||||
assert d2 == d
|
||||
|
||||
def test_multiple_paths(self):
|
||||
d = {}
|
||||
|
||||
DictMapper.set(d, ["a", "b1", "c"], 1)
|
||||
DictMapper.set(d, ["a", "b1", "d"], 2)
|
||||
DictMapper.set(d, ["a", "b2", "c"], 3)
|
||||
DictMapper.set(d, ["a", "b2", "d"], 4)
|
||||
|
||||
expected = {
|
||||
"a": {
|
||||
"b1": {
|
||||
"c": 1,
|
||||
"d": 2,
|
||||
},
|
||||
"b2": {
|
||||
"c": 3,
|
||||
"d": 4,
|
||||
}
|
||||
}
|
||||
}
|
||||
assert d == expected
|
||||
|
||||
def test_overwrite(self):
|
||||
d = {}
|
||||
|
||||
DictMapper.set(d, ["a", "b1", "c"], 1)
|
||||
DictMapper.set(d, ["a", "b1", "d"], 2)
|
||||
DictMapper.set(d, ["a", "b2", "c"], 3)
|
||||
DictMapper.set(d, ["a", "b2", "d"], 4)
|
||||
|
||||
expected = {
|
||||
"a": {
|
||||
"b1": {
|
||||
"c": 1,
|
||||
"d": 2,
|
||||
},
|
||||
"b2": {
|
||||
"c": 3,
|
||||
"d": 4,
|
||||
}
|
||||
}
|
||||
}
|
||||
assert d == expected
|
||||
|
||||
DictMapper.set(d, ["a", "b2"], 4)
|
||||
expected = {
|
||||
"a": {
|
||||
"b1": {
|
||||
"c": 1,
|
||||
"d": 2,
|
||||
},
|
||||
"b2": 4,
|
||||
}
|
||||
}
|
||||
assert d == expected
|
||||
@@ -1,57 +1,74 @@
|
||||
from time import perf_counter
|
||||
|
||||
from bs4 import BeautifulSoup as bs4
|
||||
|
||||
from bot import MusicBot
|
||||
|
||||
class ThumbNailer(object):
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
self.logger = self.bot.main_logger.bot_logger.getChild("ThumbNailer")
|
||||
|
||||
async def get_html(self, url):
|
||||
async with self.bot.session.get(url, timeout=30) as response:
|
||||
class Thumbnailer(object):
|
||||
def __init__(self, bot: MusicBot):
|
||||
self.bot: MusicBot = bot
|
||||
self.logger = self.bot.main_logger.bot_logger.getChild("Thumbnailer")
|
||||
|
||||
async def get_html(self, url: str):
|
||||
async with self.bot.session.get(url, timeout=3) as response:
|
||||
assert response.status == 200
|
||||
return await response.read()
|
||||
|
||||
@staticmethod
|
||||
async def _soundcloud(self, url):
|
||||
html = await ThumbNailer.get_html(self, url)
|
||||
async def _soundcloud(self, url: str) -> str:
|
||||
perf_start = perf_counter()
|
||||
html = await self.get_html(url)
|
||||
try:
|
||||
soup = bs4(html, 'html.parser')
|
||||
img = soup.find("meta", property="twitter:image")
|
||||
return img["content"]
|
||||
except Exception as e:
|
||||
self.logger.exception("%s" % e)
|
||||
|
||||
async def _bandcamp(self, url):
|
||||
html = await ThumbNailer.get_html(self, url)
|
||||
try:
|
||||
soup = bs4(html, 'html.parser')
|
||||
img = soup.find(class_="popupImage").get("href")
|
||||
soup = soup.find("meta", property="twitter:image")
|
||||
img = soup.get("content", default="")
|
||||
perf_stop = perf_counter()
|
||||
self.logger.debug("Took %s to find thumbnail from Soundcloud" % (perf_stop - perf_start))
|
||||
return img
|
||||
except Exception as e:
|
||||
self.logger.exception("%s" % e)
|
||||
return ""
|
||||
|
||||
async def _vimeo(self, url):
|
||||
html = await ThumbNailer.get_html(self, url)
|
||||
async def _bandcamp(self, url: str) -> str:
|
||||
perf_start = perf_counter()
|
||||
html = await self.get_html(url)
|
||||
try:
|
||||
soup = bs4(html, 'html.parser')
|
||||
img = soup.find("meta", property="og:image")
|
||||
return img["content"]
|
||||
soup = soup.find(class_="popupImage")
|
||||
img = soup.get("href", default="")
|
||||
perf_stop = perf_counter()
|
||||
self.logger.debug("Took %s to find thumbnail from Bandcamp" % (perf_stop - perf_start))
|
||||
return img
|
||||
except Exception as e:
|
||||
self.logger.exception("%s" % e)
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
async def identify(self, identifier, uri):
|
||||
async def _vimeo(self, url: str) -> str:
|
||||
perf_start = perf_counter()
|
||||
html = await self.get_html(url)
|
||||
try:
|
||||
soup = bs4(html, 'html.parser')
|
||||
soup = soup.find("meta", property="og:image")
|
||||
img = soup.get("content", default="")
|
||||
perf_stop = perf_counter()
|
||||
self.logger.debug("Took %s to find thumbnail from Vimeo" % (perf_stop - perf_start))
|
||||
return img
|
||||
except Exception as e:
|
||||
self.logger.exception("%s" % e)
|
||||
return ""
|
||||
|
||||
async def identify(self, identifier: str, uri: str) -> str:
|
||||
if "youtube" in uri:
|
||||
thumbnail_url = f"https://img.youtube.com/vi/{identifier}/0.jpg"
|
||||
return thumbnail_url
|
||||
elif "soundcloud" in uri:
|
||||
thumbnail_url = await ThumbNailer._soundcloud(self, url=uri)
|
||||
thumbnail_url = await self._soundcloud(url=uri)
|
||||
return thumbnail_url
|
||||
elif "bandcamp" in uri:
|
||||
thumbnail_url = await ThumbNailer._bandcamp(self, url=uri)
|
||||
thumbnail_url = await self._bandcamp(url=uri)
|
||||
return thumbnail_url
|
||||
elif "vimeo" in uri:
|
||||
thumbnail_url = await ThumbNailer._vimeo(self, url=uri)
|
||||
thumbnail_url = await self._vimeo(url=uri)
|
||||
return thumbnail_url
|
||||
else:
|
||||
return None
|
||||
return ""
|
||||
@@ -1,3 +1,7 @@
|
||||
# Discord Packages
|
||||
import lavalink
|
||||
|
||||
|
||||
def format_ms(milliseconds):
|
||||
seconds = int(milliseconds)/1000
|
||||
formatted = ''
|
||||
@@ -17,3 +21,7 @@ def unformat_ms(formatted):
|
||||
if len(hms) == 3:
|
||||
seconds += hms[2] * 3600
|
||||
return seconds * 1000
|
||||
|
||||
|
||||
def format_track_duration(track: lavalink.AudioTrack) -> str:
|
||||
return '{live}' if track.stream else format_ms(track.duration)
|
||||
@@ -16,7 +16,7 @@ class QueuePaginator(TextPaginator):
|
||||
duration = player.queue_duration(member=member, include_current=include_current)
|
||||
|
||||
if member:
|
||||
member_queue = player.user_queue_with_global_index(member.id)
|
||||
member_queue = player.user_queue_with_global_index(member)
|
||||
title = localizer.format_str("{queue.userqueue}",
|
||||
_user=member.display_name, _length=len(member_queue), _duration=duration)
|
||||
|
||||
|
||||
@@ -5,7 +5,9 @@ import discord
|
||||
|
||||
import asyncio
|
||||
from enum import Flag, auto
|
||||
from typing import List, Optional
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from bot import MusicBot
|
||||
|
||||
from .paginators import BasePaginator, CantScrollException
|
||||
|
||||
@@ -42,7 +44,7 @@ class Scroller:
|
||||
if not self.paginator.pages:
|
||||
raise Exception("Paginator contained no pages to display") # TODO: proper error
|
||||
|
||||
self.bot = ctx.bot
|
||||
self.bot: MusicBot = ctx.bot
|
||||
self.channel = ctx.channel
|
||||
self.message: Optional[discord.Message] = None
|
||||
|
||||
@@ -64,7 +66,7 @@ class Scroller:
|
||||
'\N{BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}', row=3)
|
||||
last_page_button = ScrollerButton(self.last_page,
|
||||
'\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}', row=3)
|
||||
self.stop_button = ScrollerButton(self.stop, stop_emoji, row=3)
|
||||
self.stop_button = ScrollerButton(self.stop_scrolling, stop_emoji, row=3)
|
||||
|
||||
# We start on the first page, can't go back
|
||||
self.back_button.disabled = True
|
||||
@@ -90,9 +92,9 @@ class Scroller:
|
||||
|
||||
async def start_scrolling(self, clear_mode: ClearOn,
|
||||
message: Optional[discord.Message] = None,
|
||||
start_page: int = 0) -> discord.Message:
|
||||
start_page: int = 0) -> Tuple[discord.Message, bool]:
|
||||
self.clear_mode = clear_mode
|
||||
self.page_number = start_page
|
||||
self.page_number = min(start_page, len(self.paginator.pages))
|
||||
self.build_view()
|
||||
self.update_view()
|
||||
if message:
|
||||
@@ -101,9 +103,9 @@ class Scroller:
|
||||
else:
|
||||
self.message = await self.channel.send(embed=self.current_page_embed, view=self.view)
|
||||
await self.scrolling_done.wait()
|
||||
return self.message
|
||||
return self.message, self.timed_out
|
||||
|
||||
def update_view_on_interaction(self, interaction: discord.Interaction):
|
||||
def update_view_on_interaction(self, _: discord.Interaction):
|
||||
self.update_view()
|
||||
|
||||
def update_view(self):
|
||||
@@ -124,13 +126,14 @@ class Scroller:
|
||||
for (i, _) in enumerate(self.paginator.pages):
|
||||
self.navigator.add_option(label=str(i+1), value=str(i))
|
||||
|
||||
async def stop(self, user_stopped: bool, clear_scroller_view: bool = True):
|
||||
async def stop(self, was_timeout: bool, clear_scroller_view: bool = True):
|
||||
self.is_scrolling_paginator = False
|
||||
self.view.stop()
|
||||
if clear_scroller_view:
|
||||
self.view.clear_items()
|
||||
if (user_stopped and self.clear_mode & ClearOn.ManualExit or
|
||||
not user_stopped and self.clear_mode & ClearOn.Timeout):
|
||||
self.timed_out = was_timeout
|
||||
if (not was_timeout and self.clear_mode & ClearOn.ManualExit or
|
||||
was_timeout and self.clear_mode & ClearOn.Timeout):
|
||||
if self.message:
|
||||
await self.message.delete()
|
||||
self.message = None
|
||||
@@ -183,10 +186,10 @@ class Scroller:
|
||||
|
||||
async def stop_scrolling(self, interaction: discord.Interaction):
|
||||
await interaction.response.defer()
|
||||
await self.stop(user_stopped=True)
|
||||
await self.stop(was_timeout=False)
|
||||
|
||||
async def on_timeout(self):
|
||||
await self.stop(user_stopped=False)
|
||||
await self.stop(was_timeout=True)
|
||||
|
||||
@property
|
||||
def current_page_embed(self):
|
||||
|
||||
@@ -84,9 +84,9 @@ class Selector(TextPaginator, Scroller):
|
||||
|
||||
match self.selector_mode:
|
||||
case SelectMode.SingleSelect:
|
||||
await self.stop(True, clear_scroller_view=True)
|
||||
await self.stop(was_timeout=False, clear_scroller_view=True)
|
||||
case SelectMode.SpanningMultiSelect:
|
||||
await self.stop(True, clear_scroller_view=False)
|
||||
await self.stop(was_timeout=False, clear_scroller_view=False)
|
||||
case _:
|
||||
await self.update_message()
|
||||
|
||||
@@ -119,5 +119,5 @@ class Selector(TextPaginator, Scroller):
|
||||
async def start_scrolling(self, clear_mode: ClearOn = ClearOn.Timeout,
|
||||
message: Optional[discord.Message] = None,
|
||||
start_page: int = 0):
|
||||
message = await super().start_scrolling(clear_mode, message, start_page)
|
||||
return message, self.callback_results
|
||||
message, timed_out = await super().start_scrolling(clear_mode, message, start_page)
|
||||
return message, timed_out, self.callback_results
|
||||
Reference in new issue
Block a user