diff --git a/bot.py b/bot.py index 42dab77..036acf5 100644 --- a/bot.py +++ b/bot.py @@ -8,6 +8,7 @@ import traceback from discord.ext import commands from cogs.utils.settings import Settings +from cogs.utils.localizer import Localizer initial_extension = [ 'cogs.cogs', @@ -35,6 +36,7 @@ class Bot(commands.Bot): description=config["description"]) self.settings = Settings(**config) + self.localizer = Localizer(self.settings.get_language_path(), "en_en") self.debug = debug for extension in initial_extension: @@ -61,7 +63,7 @@ class Bot(commands.Bot): pass elif isinstance(err, commands.NoPrivateMessage): - await ctx.send("Denne kommandoen er ikke tilgjengelig i DMs") + await ctx.send(self.localizer.format_string("{commands.not_available_dm}", setting.get_locale(ctx.guild.id))) elif isinstance(err, commands.CheckFailure): pass @@ -84,6 +86,7 @@ class Bot(commands.Bot): print(f'Version: {discord.__version__}\n') self.load_extension('cogs.music') + self.load_extension('cogs.musicevents') await self.change_presence(activity=discord.Game(type=0, name=config["playing"]), diff --git a/cogs/botsettings.py b/cogs/botsettings.py index 8a1c214..9dbc647 100644 --- a/cogs/botsettings.py +++ b/cogs/botsettings.py @@ -17,6 +17,13 @@ class BotSettings: await ctx.invoke(self.bot.get_command('help'), ctx.command.qualified_name) + @commands.guild_only() + @_set.command(name='serverlocale') + async def _set_guild_locale(self, ctx, locale): + self.settings.set_locale(ctx.guild.id, locale) + locale = self.settings.get_locale(ctx.guild.id) + await ctx.send(f'Locale set to {locale}') + @commands.guild_only() @_set.command(name='serverprefix') async def _set_guild_prefix(self, ctx, *prefixes): diff --git a/cogs/cogs.py b/cogs/cogs.py index 7b104c8..2ad85ce 100644 --- a/cogs/cogs.py +++ b/cogs/cogs.py @@ -2,6 +2,7 @@ import discord import os import json import codecs +import traceback from discord.ext import commands from cogs.utils import checks @@ -66,6 +67,7 @@ class Cogs: @checks.is_owner() async def _shutdown(self, ctx): """Logs out and stops.""" + self.bot.lavalink.players.clear() await self.bot.logout() diff --git a/cogs/misc.py b/cogs/misc.py index 19572b2..453a47a 100644 --- a/cogs/misc.py +++ b/cogs/misc.py @@ -3,16 +3,17 @@ import os import asyncio import time import random +import platform from discord.ext import commands from cogs.utils import checks +from lavalink import __version__ as LavalinkVersion class Misc: def __init__(self, bot): self.bot = bot @commands.command(name='ping', hidden=True) - @checks.is_mod() async def _ping(self, ctx): start = time.perf_counter() message = await ctx.send('Ping...') @@ -40,27 +41,83 @@ class Misc: await ctx.send(guilds) @commands.command() - async def info(self, ctx, *, channel: str=None): + async def musicinfo(self, ctx): """ - Info om LovherkBot + Info om musikkspilleren """ - avatar = self.bot.user.avatar_url_as(format=None, - static_format='png', - size=1024) - infotext = f'Shite Music Bot' - embed = discord.Embed(color=0xD9C04D) - embed.set_author(name=self.bot.user.name, icon_url=avatar) - embed.set_thumbnail(url=avatar) - embed.add_field(name="Hva?", - value=infotext, inline=False) - embed.set_footer(icon_url="https://i.imgur.com/dE6JaeT.gif", - text="Laget av /r/Norge") + embed = discord.Embed(title='{info.music.title}', color=ctx.me.color) + lavalink = self.bot.lavalink + + listeners = 0 + for guild, player in lavalink.players: + listeners += len(player.listeners) + + embed.add_field(name='{info.music.players}', value=f'{len(lavalink.players)}') + embed.add_field(name='{info.music.listeners}', value=f'{listeners}') + embed = self.bot.localizer.format_embed(embed, self.bot.settings.get_locale(ctx.guild.id)) await ctx.send(embed=embed) + + + @commands.command(name="reloadlocale") + async def reload_locale(self, ctx): + self.bot.localizer.index_localizations() + self.bot.localizer.load_localizations() + await ctx.send("Localizations reloaded.") + @commands.command() - @checks.is_even() - async def only_me(self, ctx): - await ctx.send('Only you!') + async def info(self, ctx): + """ + Info om Shite Music Bot + """ + membercount = [] + for guild in self.bot.guilds: + for member in guild.members: + if member.id in membercount: + pass + else: + membercount.append(member.id) + guilds = len(self.bot.guilds) + members = len(membercount) + now = time.time() + diff = int(now - self.bot.uptime) + days, remainder = divmod(diff, 24 * 60 * 60) + hours, remainder = divmod(remainder, 60 * 60) + minutes, seconds = divmod(remainder, 60) + avatar = self.bot.user.avatar_url_as(format=None, + static_format='png', + size=1024) + + uptimetext = f'{days}d {hours}t {minutes}m {seconds}s' + embed = discord.Embed(color=ctx.me.color) + embed.set_author(name=self.bot.user.name, icon_url=avatar) + embed.set_thumbnail(url=avatar) + embed.set_image(url='https://cdn.discordapp.com/attachments/298524946454282250/368118192251469835/vintage1turntable.png') + embed.add_field(name="{info.bot.what}", + value='{info.bot.infotext}', inline=False) + embed.set_footer(icon_url="https://cdn.discordapp.com/icons/532176350019321917/92f43a1f67308a99a30c169db4b671dd.png?size=64", + text="{info.bot.footer_text}") + embed.add_field(name="{info.bot.how}", + value='{info.bot.spectext}') + embed.add_field(name="{info.bot.how_many}", + value='{info.bot.stattext}') + embed.add_field(name="{info.bot.how_long}", + value=uptimetext) + + embed = self.bot.localizer.format_embed(embed, + self.bot.settings.get_locale(ctx.guild.id), + _python_v=platform.python_version(), + _discord_v=discord.__version__, + _lavalink_v=LavalinkVersion, + _guilds=guilds, + _members=members + ) + await ctx.send(embed=embed) + + @commands.command() + @checks.is_even() + async def only_me(self, ctx): + await ctx.send('Only you!') def setup(bot): diff --git a/cogs/music.py b/cogs/music.py index f6099e0..df98106 100644 --- a/cogs/music.py +++ b/cogs/music.py @@ -1,6 +1,5 @@ """ -This is an example cog that shows how you would make use of Lavalink.py. -This example cog requires that you have python 3.6 or higher due to the f-strings. +Music commands """ import math import re @@ -18,6 +17,8 @@ from typing import Optional from .utils.embedscroller import QueueScroller from lavasettings import * +from .utils.localizer import LocalizerWrapper + time_rx = re.compile('[0-9]+') url_rx = re.compile('https?:\\/\\/(?:www\\.)?.+') @@ -31,19 +32,14 @@ class Music: bot.lavalink.add_node(host, port, password, region, 'default-node') # Host, Port, Password, Region, Name bot.add_listener(bot.lavalink.voice_update_handler, 'on_socket_response') - bot.lavalink.add_event_hook(self.track_hook) + def getLocalizer(self, guild_id): + lang = self.bot.settings.get_locale(guild_id) + return LocalizerWrapper(self.bot.localizer, lang, "music.response") - def __unload(self): - self.bot.lavalink._event_hooks.clear() - - async def track_hook(self, event): - if isinstance(event, lavalink.events.TrackEndEvent): - pass # Send track ended message to channel. - if isinstance(event, lavalink.events.QueueEndEvent): - channel = self.bot.get_channel(event.player.fetch('channel')) - await self.check_leave_voice(channel.guild) async def __before_invoke(self, ctx): + # TODO: rewrite this thing. Probably remove ensure_voice. + guild_check = ctx.guild is not None # This is essentially the same as `@commands.guild_only()` # except it saves us repeating ourselves (and also a few lines). @@ -59,11 +55,11 @@ class Music: ws = self.bot._connection._get_websocket(guild_id) await ws.voice_state(str(guild_id), channel_id) - @commands.command(aliases=['p']) - async def play(self, ctx, *, query: str): + @commands.command(name='play', aliases=['p','spill','s','spel']) + async def _play(self, ctx, *, query: str): """ Searches and plays a song from a given query. """ player = self.bot.lavalink.players.get(ctx.guild.id) - + localizer = self.getLocalizer(ctx.guild.id) query = query.strip('<>') if not url_rx.match(query): @@ -72,9 +68,9 @@ class Music: results = await player.node.get_tracks(query) if not results or not results['tracks']: - return await ctx.send('Nothing found!') + return await ctx.send(localizer.format_str("{nothing_found}")) - embed = discord.Embed(color=0xEFD26C) + embed = discord.Embed(color=ctx.me.color) if results['loadType'] == 'PLAYLIST_LOADED': tracks = results['tracks'] @@ -82,32 +78,35 @@ class Music: for track in tracks: player.add(requester=ctx.author.id, track=track) - embed.title = 'Playlist Enqueued!' - embed.description = f'{results["playlistInfo"]["name"]} - {len(tracks)} tracks' + embed.title = '{playlist_enqued}' + embed.description = f'{results["playlistInfo"]["name"]} - {len(tracks)} {{tracks}}' + embed = localizer.format_embed(embed) await ctx.send(embed=embed) else: track = results['tracks'][0] await self.enqueue(ctx, track, embed) + embed = localizer.format_embed(embed) await ctx.send(embed=embed) if not player.is_playing: await player.play() - @commands.command() - @checks.DJ_or_alone() - async def seek(self, ctx, *, time: str): + @commands.command(name='seek', aliases=['spol']) + @checks.DJ_or(alone=True) + async def _seek(self, ctx, *, time: str): """ Seeks to a given position in a track. """ player = self.bot.lavalink.players.get(ctx.guild.id) - + localizer = self.getLocalizer(ctx.guild.id) + if not player.is_playing: - return await ctx.send('Not playing.') + return await ctx.send(localizer.format_str("{not_playing}")) if ctx.author not in player.listeners: - return await ctx.send('You have to be listening to the bot') + return await ctx.send(localizer.format_str("{have_to_listen}")) seconds = time_rx.search(time) if not seconds: - return await ctx.send('You need to specify the amount of seconds to skip!') + return await ctx.send(localizer.format_str("{seek.missing_amount}")) seconds = int(seconds.group()) * 1000 if time.startswith('-'): @@ -115,89 +114,104 @@ class Music: track_time = player.position + seconds await player.seek(track_time) + msg = localizer.format_str("{seek.track_moved}",_position=lavalink.utils.format_time(track_time)) + await ctx.send(msg) - await ctx.send(f'Moved track to **{lavalink.utils.format_time(track_time)}**') - - @commands.command(aliases=['forceskip']) - async def skip(self, ctx): + @commands.command(name='skip', aliases=['hopp']) + async def _skip(self, ctx): """ Skips the current track. """ player = self.bot.lavalink.players.get(ctx.guild.id) + localizer = self.getLocalizer(ctx.guild.id) if not player.is_playing: - return await ctx.send('Not playing.') + return await ctx.send(localizer.format_str("{not_playing}")) player.add_skipper(ctx.author) total = len(player.listeners) skips = len(player.skip_voters) if skips >= math.ceil(total/2) or player.current.requester == ctx.author.id: await player.skip() - await ctx.send('Skipped.') + await ctx.send(localizer.format_str("{skip.skipped}")) else: if skips != 0: - await ctx.send(f'{skips} out of {math.ceil(total/2)} required haters have voted to skip.') + msg = localizer.format_str("{skip.require_vote}", _skips=skips, _total=math.ceil(total/2)) + await ctx.send(msg) - @commands.command(name='skipto', aliases=['st','skip_to']) - @checks.DJ_or_alone() - async def skip_to(self, ctx, pos: int): + @commands.command(name='skipto', aliases=['forceskip','hopptil','tvinghopp']) + @checks.DJ_or(alone=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.players.get(ctx.guild.id) + localizer = self.getLocalizer(ctx.guild.id) if ctx.author not in player.listeners: - return await ctx.send('You have to be listening to the bot') + return await ctx.send(localizer.format_str("{have_to_listen}")) if pos < 1: - return await ctx.send('Invalid specified position.') + return await ctx.send(localizer.format_str("{skip_to.invalid_pos}")) if len(player.queue) < pos: - return await ctx.send('The position exceeds the queue\'s length.') + return await ctx.send(localizer.format_str("{skip_to.exceeds_queue}")) await player.skip(pos - 1) - await ctx.send(f'skipped to `{player.current.title}` at position `{pos}`') + msg = localizer.format_str("{skip_to.skipped_to}", _title=player.current.title, _pos=pos) + print(msg, player.current.title, pos) + await ctx.send(msg) - @commands.command() - @checks.DJ_or_alone() - async def stop(self, ctx): + @commands.command(name='stop', aliases=['stopp']) + @checks.DJ_or(alone=True) + async def _stop(self, ctx): """ Stops the player and clears its queue. """ player = self.bot.lavalink.players.get(ctx.guild.id) + localizer = self.getLocalizer(ctx.guild.id) if ctx.author not in player.listeners: - return await ctx.send('You have to be listening to the bot') + return await ctx.send(localizer.format_str("{have_to_listen}")) if not player.is_playing: - return await ctx.send('Not playing.') + return await ctx.send(localizer.format_str("{not_playing}")) player.queue.clear() await player.stop() - await ctx.send('⏹ | Stopped.') + await ctx.send(localizer.format_str("{stop}")) - @commands.command(aliases=['np', 'n', 'playing']) - async def now(self, ctx): + @commands.command(name='now', aliases=['np','current', 'nå', 'nåspilles', 'gjeldende', 'spillernå','spelarno','nospelar']) + async def _now(self, ctx): """ Shows some stats about the currently playing song. """ player = self.bot.lavalink.players.get(ctx.guild.id) + localizer = self.getLocalizer(ctx.guild.id) if not player.current: - return await ctx.send('Nothing playing.') + return await ctx.send(localizer.format_str("{not_playing}")) position = lavalink.utils.format_time(player.position) if player.current.stream: - duration = '🔴 LIVE' + duration = '{live}' else: duration = lavalink.utils.format_time(player.current.duration) song = f'**[{player.current.title}]({player.current.uri})**\n({position}/{duration})' - embed = discord.Embed(color=0xEFD26C, - title='Now Playing', description=song) - + member = ctx.guild.get_member(player.current.requester) + embed = discord.Embed(color=ctx.me.color, description=song, title='{now}') thumbnail_url = await RoxUtils.ThumbNailer.identify(self,player.current.identifier, player.current.uri) if thumbnail_url: embed.set_thumbnail(url=thumbnail_url) + + if member.nick: + member_identifier = member.nick + else: + member_identifier = member.name + embed.set_footer(text=f'{{requested_by}} {member_identifier}', icon_url=member.avatar_url) + + embed = localizer.format_embed(embed) await ctx.send(embed=embed) - @commands.command(aliases=['q']) - async def queue(self, ctx, user: discord.Member=None): + @commands.command(name='queue', aliases=['q', 'kø']) + async def _queue(self, ctx, user: discord.Member=None): """ Shows the player's queue. """ player = self.bot.lavalink.players.get(ctx.guild.id) + localizer = self.getLocalizer(ctx.guild.id) if player.queue.empty: - return await ctx.send('The queue is empty') + return await ctx.send(localizer.format_str("{queue.empty}")) if user is None: queue = player.global_queue() @@ -207,73 +221,80 @@ class Music: else: user_queue = player.user_queue(user.id, dual=True) if not user_queue: - return await ctx.send(f'{user.name}\'s queue is empty') + return await ctx.send(localizer.format_str("{queue.empty}", _user=user.name)) scroller = QueueScroller(ctx, user_queue, lines_per_page=10, user_name=user.name) await scroller.start_scrolling() - @commands.command(name='myqueue', aliases=['mq']) + @commands.command(name='myqueue', aliases=['mq','minkø','mk']) async def _myqueue(self, ctx): """ Shows your queue. """ player = self.bot.lavalink.players.get(ctx.guild.id) + localizer = self.getLocalizer(ctx.guild.id) user_queue = player.user_queue(ctx.author.id, dual=True) if not user_queue: - return await ctx.send('Your queue is empty') + return await ctx.send(localizer.format_str("{my_queue}")) scroller = QueueScroller(ctx, user_queue, lines_per_page=10, user_name=ctx.author.name) await scroller.start_scrolling() - @commands.command(aliases=['resume']) - @checks.is_DJ() - async def pause(self, ctx): + @commands.command(name='pause', aliases=['resume','gjenoppta']) + @checks.DJ_or(alone=True) + async def _pause(self, ctx): """ Pauses/Resumes the current track. """ player = self.bot.lavalink.players.get(ctx.guild.id) + localizer = self.getLocalizer(ctx.guild.id) if not player.is_playing: - return await ctx.send('Not playing.') + return await ctx.send(localizer.format_str("{not_playing}")) if player.paused: await player.set_pause(False) - await ctx.send('⏯ | Resumed') + await ctx.send(localizer.format_str("{resume.resumed}")) else: await player.set_pause(True) - await ctx.send('⏯ | Paused') + await ctx.send(localizer.format_str("{resume.paused}")) - @commands.command(name='shuffle') + @commands.command(name='shuffle', aliases=['stokk']) async def _shuffle(self, ctx): """ Shuffles your queue. """ player = self.bot.lavalink.players.get(ctx.guild.id) + localizer = self.getLocalizer(ctx.guild.id) + user_queue = player.user_queue(ctx.author.id) if not user_queue: - return await ctx.send('Your queue is empty') + return await ctx.send(localizer.format_str("{my_queue}")) player.shuffle_user_queue(ctx.author.id) - await ctx.send('Your queue has been shuffled') + await ctx.send(localizer.format_str("{shuffle}")) - @commands.command(name='move', aliases=["m"]) + @commands.command(name='move', aliases=['m', 'flytt','f']) async def _move(self, ctx, from_pos: int, to_pos: int): """ Moves a song in your queue. """ player = self.bot.lavalink.players.get(ctx.guild.id) + localizer = self.getLocalizer(ctx.guild.id) user_queue = player.user_queue(ctx.author.id) if not user_queue: - return await ctx.send('Your queue is empty') + return await ctx.send(localizer.format_str("{my_queue}")) - if not all(x in range(1,len(user_queue)+1) for x in [from_pos, to_pos]): - return await ctx.send(f'Positions must be between 1 and {len(user_queue)}') + if not all(x in range(1,len(user_queue) + 1) for x in [from_pos, to_pos]): + return await ctx.send(localizer.format_str("{out_of_range}", _len=len(user_queue))) moved = player.move_user_track(ctx.author.id, from_pos - 1, to_pos - 1) if moved is None: - return await ctx.send('Check that the positions exist in your queue') + return await ctx.send(localizer.format_str("{move.not_in_queue}")) - await ctx.send(f'{moved.title} moved from position {from_pos} to {to_pos} in your queue') + msg = localizer.format_str("{move.moved_to}", _title=moved.title, _from=from_pos, _to=to_pos) + await ctx.send(msg) - @commands.command() - async def remove(self, ctx, pos: int): + @commands.command(name='remove', aliases=['rem', 'fjern']) + async def _remove(self, ctx, pos: int): """ Removes an item from the player's queue with the given pos. """ player = self.bot.lavalink.players.get(ctx.guild.id) + localizer = self.getLocalizer(ctx.guild.id) if player.queue.empty: return @@ -283,28 +304,29 @@ class Music: return if pos > len(user_queue) or pos < 1: - return await ctx.send(f'Position has to be between 1 and {len(user_queue)}') + return await ctx.send(localizer.format_str("{out_of_range}", _len=len(user_queue))) removed = player.remove_user_track(ctx.author.id, pos - 1) - await ctx.send(f'**{removed.title}** removed.') + await ctx.send(localizer.format_str("{remove}", _title=removed.title)) - @commands.command(name="DJremove") - @checks.is_DJ() + @commands.command(name="DJremove", aliases=['DJfjern','djfjern']) + @checks.DJ_or() async def _djremove(self, ctx, pos: int, user: discord.Member=None): """ Remove a song from either the global queue or a users queue""" player = self.bot.lavalink.players.get(ctx.guild.id) + localizer = self.getLocalizer(ctx.guild.id) if user is None: if player.queue.empty: return if pos > len(player.queue) or pos < 1: - return await ctx.send(f'Position has to be between 1 and {len(player.queue)}') + return await ctx.send(localizer.format_str("{out_of_range}", _len=len(player.queue))) removed = player.remove_global_track(pos - 1) requester = self.bot.get_user(removed.requester) - await ctx.send(f'**{removed.title}** queued by {requester.name} removed.') + await ctx.send(localizer.format_str("{dj_removed}", _title=removed.title, _user=requester.name)) else: if player.queue.empty: @@ -315,16 +337,17 @@ class Music: return if pos > len(user_queue) or pos < 1: - return await ctx.send(f'Position has to be between 1 and {len(user_queue)}') + return await ctx.send(localizer.format_str("{out_of_range}", _len=len(user_queue))) removed = player.remove_user_track(user.id, pos - 1) - await ctx.send(f'**{removed.title}** queued by {user.name} removed.') + await ctx.send(localizer.format_str("{dj_removed}", _title=removed.title, _user=requester.name)) - @commands.command(name="removeuser") - @checks.DJ_or_alone() + @commands.command(name='removeuser', aliases=['fjernbruker','fjernbrukar']) + @checks.DJ_or(alone=True) async def _user_queue_remove(self, ctx, user: discord.Member): """ Remove a song from either the global queue or a users queue""" player = self.bot.lavalink.players.get(ctx.guild.id) + localizer = self.getLocalizer(ctx.guild.id) if player.queue.empty: return @@ -334,22 +357,27 @@ class Music: return player.remove_user_queue(user.id) - embed = discord.Embed(color=0x36393F) - embed.description = f'Removed all songs queued by <@{user.id}>.' + + embed = discord.Embed(color=ctx.me.color) + embed.description = "{dj_remove_user}" + embed = localizer.format_embed(embed, _id=user.id) + await ctx.send(embed=embed) - @commands.command() - async def search(self, ctx, *, query): + @commands.command(name='search', aliases=['søk','finn']) + async def _search(self, ctx, *, query): """ Lists the first 10 search results from a given query. """ player = self.bot.lavalink.players.get(ctx.guild.id) + localizer = self.getLocalizer(ctx.guild.id) 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) + embed = discord.Embed(description='{nothing_found}', color=0x36393F) if not results or not results['tracks']: + embed = localizer.format_embed(embed) return await ctx.send(embed=embed) tracks = results['tracks'] @@ -389,7 +417,8 @@ class Music: return True return False - embed.description = 'searching' + embed.description = '{search}' + embed = localizer.format_embed(embed) result_msg = await ctx.send(embed=embed) for emoji, index in choices[:result_count]: @@ -397,7 +426,11 @@ class Music: await result_msg.add_reaction('❌') embed.description = search_results - embed.title = 'Results' + + embed.title = '{results}' + embed.color = ctx.me.color + embed = localizer.format_embed(embed) + await result_msg.edit(embed=embed) try: @@ -407,7 +440,8 @@ class Music: except asyncio.TimeoutError: await result_msg.clear_reactions() embed.title = '' - embed.description='Timer Expired' + embed.description='{time_expired}' + embed = localizer.format_embed(embed) await result_msg.edit(embed=embed) await asyncio.sleep(5) await result_msg.delete() @@ -418,88 +452,120 @@ class Music: await result_msg.clear_reactions() track = tracks[choice - 1] await self.enqueue(ctx, track, embed) + embed = localizer.format_embed(embed) await result_msg.edit(embed=embed) if not player.is_playing: await player.play() - @commands.command(aliases=['dc']) - @checks.DJ_or_alone() - async def disconnect(self, ctx): + @commands.command(name='disconnect', aliases=['dc','kf','koblefra','koblefrå']) + @checks.DJ_or(alone=True) + async def _disconnect(self, ctx): """ Disconnects the player from the voice channel and clears its queue. """ player = self.bot.lavalink.players.get(ctx.guild.id) + localizer = self.getLocalizer(ctx.guild.id) if not player.is_connected: - return await ctx.send('Not connected.') + return await ctx.send(localizer.format_str("{not_connected}")) if not ctx.author.voice or (player.is_connected and ctx.author.voice.channel.id != int(player.channel_id)): - return await ctx.send('You\'re not in my voicechannel!') + return await ctx.send(localizer.format_str("{disconnect.not_in_voice}")) player.queue.clear() await player.stop() await self.connect_to(ctx.guild.id, None) - embed = discord.Embed(description='Disconnected', color=0x36393F) + embed = discord.Embed(description='{disconnect.disconnected}', color=ctx.me.color) + embed = localizer.format_embed(embed) await ctx.send(embed=embed) - @commands.command(aliases=['vol']) - @checks.DJ_or_current() - async def volume(self, ctx, volume: int = None): + @commands.command(name='volume', aliases=['vol', 'volum', 'lydstyrke']) + @checks.DJ_or(alone=True, current=True) + async def _volume(self, ctx, volume: 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.players.get(ctx.guild.id) - - # todo: limit to 50%-125% if user has current song. + localizer = self.getLocalizer(ctx.guild.id) if not volume: return await ctx.send(f'🔈 | {player.volume}%') + if int(player.current.requester) == ctx.author.id: + if not 50 <= volume <= 125: + return await ctx.send(localizer.format_str("{volume.out_of_range}")) + await player.set_volume(volume) - embed = discord.Embed(color=0x36393F) - embed.description = f'Volume set to {player.volume}' + embed = discord.Embed(color=ctx.me.color) + embed.description = "{volume.set_to}" + embed = localizer.format_embed(embed, _volume=player.volume) await ctx.send(embed=embed) - @commands.command(aliases=['normal','nl']) - @checks.DJ_or_alone() - async def normalize(self, ctx): + @commands.command(name='nomalize', aliases=['normal','nl','normaliser']) + @checks.DJ_or(alone=True) + async def _normalize(self, ctx): """ Reset the equalizer and """ player = self.bot.lavalink.players.get(ctx.guild.id) + localizer = self.getLocalizer(ctx.guild.id) await player.set_volume(100) await player.bassboost(False) - embed = discord.Embed(color=0x36393F) - embed.description = 'Volume and equalizer reset' + + embed = discord.Embed(color=ctx.me.color) + embed.description = '{volume.reset}' + embed = localizer.format_embed(embed) + await ctx.send(embed=embed) - @commands.command(name='boost', aliases=['boo']) - @checks.DJ_or_alone() + @commands.command(name='boost', aliases=['boo','bassforsterker','bassforsterkar']) + @checks.DJ_or(alone=True) async def _boost(self, ctx, boost: bool=None): """ Set the equalizer to bass boost the music """ player = self.bot.lavalink.players.get(ctx.guild.id) - + localizer = self.getLocalizer(ctx.guild.id) + if boost is not None: await player.bassboost(boost) - embed = discord.Embed(color=0x36393F) + embed = discord.Embed(color=ctx.me.color) if player.boosted: - embed.description = 'Bass boost is on' - await ctx.send(embed=embed) + embed.description = '{boost.on}' else: - embed.description = 'Bass boost is off' - await ctx.send(embed=embed) + embed.description = '{boost.off}' + + embed = localizer.format_embed(embed) + await ctx.send(embed=embed) - @commands.command() - @checks.DJ_or_alone() - async def scrub(self, ctx): + @commands.command(name='history', aliases=['h','hist','historie']) + async def _history(self, ctx): + """ Show the last 10 songs played """ + player = self.bot.lavalink.players.get(ctx.guild.id) + localizer = self.getLocalizer(ctx.guild.id) + history = player.get_history() + track = history[0] + description = localizer.format_str("{history.current}", _title=track.title, _uri=track.uri,_id=track.requester) + '\n\n' + description += localizer.format_str("{history.previous}", _len=len(history)-1) + '\n' + thumb_url = await RoxUtils.ThumbNailer.identify(self, + track.identifier, + track.uri) + for index, track in enumerate(history[1:], start=1): + description += localizer.format_str("{history.track}",_index=-index, _title=track.title, _uri=track.uri, _id=track.requester) + '\n' + + embed = discord.Embed(title=localizer.format_str('{history.title}'), color=ctx.me.color, description=description) + + if thumb_url: + embed.set_thumbnail(url=thumb_url) + await ctx.send(embed=embed) + + + @commands.command(name='scrub', aliases=['kontrol','konsoll']) + @checks.DJ_or(alone=True) + async def _scrub(self, ctx): """ Lists the first 10 search results from a given query. """ player = self.bot.lavalink.players.get(ctx.guild.id) + localizer = self.getLocalizer(ctx.guild.id) - controls = ''' - **Controls** - Play/pause: \N{BLACK RIGHT-POINTING TRIANGLE} \N{DOUBLE VERTICAL BAR} - skip 15 seconds forward/backward: \N{BLACK LEFT-POINTING DOUBLE TRIANGLE} \N{BLACK RIGHT-POINTING DOUBLE TRIANGLE} - Skip to beginning/end of song: \N{BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR} \N{BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR} + controls = '{scrub.controls}' + embed = discord.Embed(description='{nothing_playing}', color=ctx.me.color) + embed = localizer.format_embed(embed) - ''' - embed = discord.Embed(description='Nothing playing', color=0x36393F) if player.current is None: return await ctx.send(embed=embed) @@ -531,13 +597,15 @@ class Music: return True return False - embed.description = 'Adding controls' + embed.description = '{scrub.add}' + embed = localizer.format_embed(embed) msg = await ctx.send(embed=embed) for (emoji, _, _) in scrubber: await msg.add_reaction(emoji) embed.description = controls + embed = localizer.format_embed(embed) await msg.edit(embed=embed) scrubbing = True while scrubbing: @@ -586,33 +654,10 @@ class Music: player.store('channel', ctx.channel.id) await self.connect_to(ctx.guild.id, str(ctx.author.voice.channel.id)) - # Get listeners - voice_channel = ctx.author.voice.channel - while not player.is_connected: - await asyncio.sleep(0.5) - - for member in voice_channel.members: - if not member.bot: - player.update_listeners(member, member.voice) - else: if int(player.channel_id) != ctx.author.voice.channel.id: raise commands.CommandInvokeError('You need to be in my voicechannel.') - async def on_voice_state_update(self, member, before, after): - if not member.bot: - player = self.bot.lavalink.players.get(member.guild.id) - if player is not None: - player.update_listeners(member, after) - await self.check_leave_voice(member.guild) - - async def check_leave_voice(self, guild): - player = self.bot.lavalink.players.get(guild.id) - if len(player.listeners) == 0 and player.is_connected: - if player.queue.empty and player.current is None: - await player.stop() - await self.connect_to(guild.id, None) - def max_track_length(self, guild): player = self.bot.lavalink.players.get(guild.id) if len(player.listeners): @@ -626,6 +671,7 @@ class Music: player = self.bot.lavalink.players.get(ctx.guild.id) track, pos_global, pos_local = player.add(requester=ctx.author.id, track=track) + localizer = self.getLocalizer(ctx.guild.id) if player.current is not None: queue_duration = 0 @@ -636,10 +682,10 @@ class Music: until_play = queue_duration + player.current.duration - player.position until_play = lavalink.utils.format_time(until_play) - embed.add_field(name="Position", value=f"`{pos_local + 1}({pos_global + 1})`", inline=True) - embed.add_field(name="Playing in", value=f"`{until_play} (estimated)`", inline=True) + embed.add_field(name="{enqueue.position}", value=f"`{pos_local + 1}({pos_global + 1})`", inline=True) + embed.add_field(name="{enqueue.playing_in}", value=f"`{until_play} ({{enqueue.estimated}})`", inline=True) - embed.title = 'Song enqueued' + embed.title = '{enqueue.enqueued}' thumb_url = await RoxUtils.ThumbNailer.identify(self, track.identifier, track.uri) @@ -650,6 +696,5 @@ class Music: duration = lavalink.utils.format_time(int(track.duration)) embed.description = f'[{track.title}]({track.uri})\n**{duration}**' - def setup(bot): bot.add_cog(Music(bot)) diff --git a/cogs/musicevents.py b/cogs/musicevents.py new file mode 100644 index 0000000..1caa8a0 --- /dev/null +++ b/cogs/musicevents.py @@ -0,0 +1,81 @@ +""" +A cog to separate events from regular music commands +""" + +import asyncio + +import discord +import lavalink +from discord.ext import commands +import time + +from .utils.mixplayer import MixPlayer +from lavalink.events import * + +from lavasettings import * + + +class MusicEvents: + def __init__(self, bot): + self.bot = bot + # TODO: maybe only load when Music loads + if not hasattr(bot, 'lavalink'): # This ensures the client isn't overwritten during cog reloads. + bot.lavalink = lavalink.Client(bot.user.id, player=MixPlayer) + bot.lavalink.add_node(host, port, password, region, 'default-node') # Host, Port, Password, Region, Name + bot.add_listener(bot.lavalink.voice_update_handler, 'on_socket_response') + + bot.lavalink.add_event_hook(self.track_hook) + + def __unload(self): + self.bot.lavalink._event_hooks.clear() + + async def track_hook(self, event): + if isinstance(event, TrackEndEvent): + pass # Send track ended message to channel. + 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 isinstance(event, PlayerUpdateEvent): + pass + if isinstance(event, NodeDisconnectedEvent): + pass + if isinstance(event, NodeConnectedEvent): + pass + if isinstance(event, NodeChangedEvent): + pass + + async def connect_to(self, guild_id: int, channel_id: str): + """ Connects to the given voicechannel ID. A channel_id of `None` means disconnect. """ + ws = self.bot._connection._get_websocket(guild_id) + await ws.voice_state(str(guild_id), channel_id) + + async def on_voice_state_update(self, member, before, after): + """ Updates listeners when the bot or a user changes voice state """ + if member.id == self.bot.user.id and after.channel is not None: + voice_channel = after.channel + player = self.bot.lavalink.players.get(member.guild.id) + 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.players.get(member.guild.id) + if player is not None: + player.update_listeners(member, after) + await self.check_leave_voice(member.guild) + + async def check_leave_voice(self, guild): + """ Checks if the bot should leave the voice channel """ + # TODO, disconnect timer? + player = self.bot.lavalink.players.get(guild.id) + if len(player.listeners) == 0 and player.is_connected: + if player.queue.empty and player.current is None: + await player.stop() + await self.connect_to(guild.id, None) + + +def setup(bot): + bot.add_cog(MusicEvents(bot)) diff --git a/cogs/utils/checks.py b/cogs/utils/checks.py index b771fb1..247a817 100644 --- a/cogs/utils/checks.py +++ b/cogs/utils/checks.py @@ -37,7 +37,7 @@ def is_even(): def is_owner(): def predicate(ctx): is_owner = (ctx.message.author.id == 120970603556503552 or - ctx.message.author.id == 142212883512557569) + ctx.message.author.id == 142212883512557569 or ctx.message.author.id == 212635519706726410) return is_owner return commands.check(predicate) @@ -55,35 +55,18 @@ def is_mod(): return commands.check(pred) -def is_DJ(): - async def predicate(ctx): - is_dj = has_role(ctx, 'DJ') or has_role(ctx, 'dj') or has_role(ctx, 'Dj') - is_admin = await check_guild_permissions(ctx, {'administrator': True}) - return is_dj or is_admin - return commands.check(predicate) - - -def DJ_or_current(): +def DJ_or(alone: bool=False, current: bool=False): async def predicate(ctx): try: player = ctx.bot.lavalink.players.get(ctx.guild.id) - requester = player.current.requester + is_alone = (ctx.author in player.listeners and len(player.listeners) == 1) and alone + + requester = (player.current.requester == ctx.author.id) and current + is_dj = has_role(ctx, 'DJ') or has_role(ctx, 'dj') or has_role(ctx, 'Dj') is_admin = await check_guild_permissions(ctx, {'administrator': True}) - return is_dj or is_admin or requester == ctx.author.id - except AttributeError: - return False - return commands.check(predicate) - -def DJ_or_alone(): - async def predicate(ctx): - try: - player = ctx.bot.lavalink.players.get(ctx.guild.id) - alone = ctx.author in player.listeners and len(player.listeners) == 1 - is_dj = has_role(ctx, 'DJ') or has_role(ctx, 'dj') or has_role(ctx, 'Dj') - is_admin = await check_guild_permissions(ctx, {'administrator': True}) - return is_dj or is_admin or alone + return is_dj or is_admin or is_alone or requester except AttributeError: return False return commands.check(predicate) diff --git a/cogs/utils/dict_utils.py b/cogs/utils/dict_utils.py new file mode 100644 index 0000000..b3e6439 --- /dev/null +++ b/cogs/utils/dict_utils.py @@ -0,0 +1,17 @@ +import collections + +class SafeDict(dict): + def __missing__(self, key): + return '{' + key + '}' + + +# from https://stackoverflow.com/questions/6027558/flatten-nested-python-dictionaries-compressing-keys +def flatten(d, parent_key='', sep='/'): + items = [] + for k, v in d.items(): + new_key = parent_key + sep + k if parent_key else k + if isinstance(v, collections.MutableMapping): + items.extend(flatten(v, new_key, sep=sep).items()) + else: + items.append((new_key, v)) + return dict(items) diff --git a/cogs/utils/localizer.py b/cogs/utils/localizer.py new file mode 100644 index 0000000..c4e27cc --- /dev/null +++ b/cogs/utils/localizer.py @@ -0,0 +1,167 @@ +from glob import glob +from os import path + +import re + +from cogs.utils.dict_utils import flatten, SafeDict + +import json + +import copy + +from discord import Embed + +""" +Localizer for bot +""" + +kmatch = re.compile('({(?!_)([^{}]+)})') + +class Localizer: + def __init__(self, localization_folder, default_lang): + self.localization_folder = path.realpath(localization_folder) + self.default_lang = default_lang + # look for localization folders + self.index_localizations() + # load localizations + self.load_localizations() + + # indexes localization folder + def index_localizations(self): + self.localization_table = {} + for folder in glob(path.join(self.localization_folder, "*/")): + folder_base = path.basename(path.dirname(folder)) + self.localization_table[folder_base] = False + + + # loads all localizations + def load_localizations(self): + for lang in self.localization_table.keys(): + self.localization_table[lang] = False + self._load_localization(lang) + + self.all_localizations = flatten(self.localization_table) + for lang, d in self.localization_table.items(): + self.localization_table[lang] = Localizer._parse_localization_dictionary(self.localization_table[lang], self.all_localizations) + + # internal function for loading a localization + def _load_localization(self, lang): + localization = self.localization_table.get(lang) + if localization is None: + raise Exception(f'Localization for {lang} does not exist') + elif localization is False: + self.localization_table[lang] = {} + l_table = self.localization_table[lang] + for file in glob(path.join(self.localization_folder, lang, "*.json")): + file_base = path.basename(file).split(".")[0] + with open(file, "r", encoding='utf-8') as f: + data = json.load(f) + + l_table[file_base] = data + + for file in glob(path.join(self.localization_folder, lang, "*.txt")): + file_base = path.basename(file).split(".")[0] + with open(file, "r", encoding='utf-8') as f: + content = f.read() + l_table[file_base] = content + + + l_table = flatten(l_table) + # parsing a few times to resolve all values + for i in range(0, 5): + l_table = Localizer._parse_localization_dictionary(l_table, l_table) + + self.localization_table[lang] = Localizer._parse_localization_dictionary(l_table, l_table) + + # parses and interpolates translation dictionary + @staticmethod + def _parse_localization_dictionary(d, lookup, prefix=None): + n_dict = {} + for k, v in d.items(): + if type(v) is str: + n_dict[k] = Localizer._parse_localization_string(v, lookup, prefix) + else: + n_dict[k] = v + return n_dict + + @staticmethod + def _replace_keys(value, prefix=None): + for outer, inner in kmatch.findall(value): + nstr = inner + if prefix is not None: + nstr = f'{prefix}.{inner}' + nstr = f'{{{nstr}}}' + nstr = nstr.replace(".", "/") + value = value.replace(outer, nstr) + return value + # parses and interpolates strings + @staticmethod + def _parse_localization_string(value, d, prefix=None): + d = SafeDict(d) + value = Localizer._replace_keys(value, prefix) + return value.format_map(d) + + # returns true if localization is currently loaded + def isLoaded(self, lang): + return self.localization_table.get(lang, False) + + def getAvaliableLocalizations(self): + return self.localization_table.keys() + + # returns translation string from a key + def get(self, key, lang=None): + lang = lang or self.default_lang + if not self.isLoaded(lang): + self._load_localization(lang) + + return self.localization_table.get(lang, {}).get(key.replace(".", "/")) + + # inserts translations into a string + def format_str(self, s, lang=None, prefix=None, **kvpairs): + lang = lang or self.default_lang + if not self.isLoaded(lang): + self._load_localization(lang) + + ns = Localizer._parse_localization_string(s, self.localization_table.get(lang, {}), prefix) + ns = Localizer._parse_localization_string(ns, self.all_localizations, prefix) + return ns.format_map(SafeDict(kvpairs)) + + # inserts translations into a values of a dictionary + def format_dict(self, d, lang=None, prefix=None, **kvpairs): + lang = lang or self.default_lang + if not self.isLoaded(lang): + self._load_localization(lang) + + nd = copy.deepcopy(d) + cursorQueue = [nd] + while cursorQueue: + cursor = cursorQueue.pop() + for k, v in (cursor.items() if type(cursor) == dict else enumerate(cursor)): + if type(v) == str: + # insert translations based on lang + cursor[k] = self.format_str(v, lang, prefix, **kvpairs) + elif type(v) == dict or type(v) == list: + cursorQueue.append(v) + + return nd + + # inserts translations into a values of a embed + def format_embed(self, embed, lang=None, prefix=None, **kvpairs): + raw = embed.to_dict() + return Embed.from_data(self.format_dict(raw, lang, prefix, **kvpairs)) + + +class LocalizerWrapper: + def __init__(self, localizer, lang=None, prefix=None): + self.localizer = localizer + self.lang = lang + self.prefix = prefix + + def format_str(self, s, **kvpairs): + return self.localizer.format_str(s, self.lang, self.prefix, **kvpairs) + + def format_dict(self, d, **kvpairs): + return self.localizer.format_dict(d, self.lang, self.prefix, **kvpairs) + + def format_embed(self, embed, **kvpairs): + return self.localizer.format_embed(embed, self.lang, self.prefix, **kvpairs) \ No newline at end of file diff --git a/cogs/utils/mixplayer.py b/cogs/utils/mixplayer.py index 7eccea7..4643add 100644 --- a/cogs/utils/mixplayer.py +++ b/cogs/utils/mixplayer.py @@ -1,7 +1,7 @@ import time import lavalink -from collections import OrderedDict +from collections import OrderedDict, deque from itertools import cycle, islice, chain from random import shuffle @@ -57,6 +57,9 @@ class MixPlayer(DefaultPlayer): def global_queue(self): return self.queue.get_queue() + def get_history(self): + return self.queue.history + def queue_duration(self, include_current: bool=True): duration = 0 for track in self.queue: @@ -108,10 +111,20 @@ class MixPlayer(DefaultPlayer): else: self.listeners.add(member) + def clear_listeners(self): + self.listeners.clear() + def add_skipper(self, member): if member in self.listeners: self.skip_voters.add(member) + async def handle_event(self, event): + """ Handles the given event as necessary. """ + if isinstance(event, (TrackStuckEvent, TrackExceptionEvent)) or \ + isinstance(event, TrackEndEvent) and event.reason == 'FINISHED': + self.skip_voters.clear() + await self.play() + async def bassboost(self, boost: bool=False): if boost: gains = [ @@ -148,6 +161,7 @@ class MixQueue: def __init__(self): self.queues = OrderedDict() self.priority_queue = [] + self._history = deque(maxlen=11) # 10 + current def __str__(self): tmp = '' @@ -190,11 +204,13 @@ class MixQueue: def pop_first(self): if self.priority_queue: next_track = self.priority_queue.pop(0) + self._history.append(next_track) return next_track try: next_track = self.queues[self.first_queue].pop(0) self._shuffle() self._clear_empty() + self._history.append(next_track) return next_track except KeyError: pass @@ -264,30 +280,34 @@ class MixQueue: for i in to_remove: self.queues.pop(i) - # Convert between global and local queue positions - # Pretty shit atm, todo: implement algorithm thing I made. - def _loc_to_glob(self, requester: int, pos: int): - queue = self.queues.get(requester, []) - if queue: - try: - track = queue[pos] - for i, t in enumerate(self): - if t == track: - return i - except IndexError: - pass + def _loc_to_glob(self, requester, pos): + globpos = len(self.priority_queue) + passed = False + for key, queue in self.queues.items(): + if len(queue) <= pos: + globpos += len(queue) + else: + globpos += pos + if key == requester: + passed = True + if not passed: + globpos += 1 + return globpos def _glob_to_loc(self, pos: int): - track = None - for i, t in enumerate(self): - if i == pos: - track = t - if track is None: + try: + song = next(islice(self, pos, pos + 1)) + except (ValueError, StopIteration): return None, None - for requester, q in self.queues.items(): - for i, t in enumerate(q): - if t == track: - return requester, i + user_queue = self.queues.get(song.requester, []) + + # In case song is in the priority queue + if not user_queue or song not in user_queue: + return None, pos + + for i, s in enumerate(user_queue): + if s == song: + return song.requester, i @property def first_queue(self): @@ -299,3 +319,8 @@ class MixQueue: @property def empty(self): return len(self) == 0 + + @property + def history(self): + return list(reversed(self._history)) + diff --git a/cogs/utils/settings.py b/cogs/utils/settings.py index 1ebc762..bd7c785 100644 --- a/cogs/utils/settings.py +++ b/cogs/utils/settings.py @@ -1,7 +1,7 @@ import os import json import codecs - +import locale class Settings: def __init__(self, **default_settings): @@ -10,13 +10,19 @@ class Settings: self.default_prefix = default_settings["default_prefix"] self.default_admin = default_settings["default_admin"] self.default_mod = default_settings["default_mod"] + self.default_locale = default_settings["default_locale"] + self.default_lang_path = default_settings["default_lang_path"] or "./localization" + if not self.default_locale: + locale, codepage = locale.getlocale() + default_locale, default_codepage = locale.getdefaultlocale() + self.default_locale = locale or default_locale if not os.path.exists(self.DATA_PATH): os.makedirs(self.DATA_PATH) if not os.path.isfile(self.SETTINGS_PATH): with codecs.open(self.SETTINGS_PATH, "w+", encoding='utf8') as f: - json.dump({"prefixes": {}, + json.dump({"locale": {}, "prefixes": {}, "roles": {}}, f, indent=4) with codecs.open(self.SETTINGS_PATH, "r", encoding='utf8') as f: @@ -60,3 +66,16 @@ class Settings: with codecs.open(self.SETTINGS_PATH, "w", encoding='utf8') as f: json.dump(self.settings, f, indent=4) + + def set_locale(self, guild_id, locale): + guild_id = str(guild_id) + self.settings["locale"][guild_id] = locale + with codecs.open(self.SETTINGS_PATH, "w", encoding='utf8') as f: + json.dump(self.settings, f, indent=4) + + def get_locale(self, guild_id): + guild_id = str(guild_id) + return self.settings.get("locale", {}).get(guild_id, self.default_locale).lower() + + def get_language_path(self): + return self.settings.get("language_path", self.default_lang_path) \ No newline at end of file diff --git a/config.json.example b/config.json.example index ad2d1eb..cdeae9f 100644 --- a/config.json.example +++ b/config.json.example @@ -3,6 +3,8 @@ "default_prefix": [""], "default_admin": "Admin", "default_mod": "Moderator", + "default_locale": "en_en", + "default_lang_path": "./localization", "playing": "", "description": "" } \ No newline at end of file diff --git a/localization/en_en/commands.json b/localization/en_en/commands.json new file mode 100644 index 0000000..0f031d3 --- /dev/null +++ b/localization/en_en/commands.json @@ -0,0 +1,3 @@ +{ + "not_available_dm": "This command is not available in DMs" +} \ No newline at end of file diff --git a/localization/en_en/info.json b/localization/en_en/info.json new file mode 100644 index 0000000..401507f --- /dev/null +++ b/localization/en_en/info.json @@ -0,0 +1,18 @@ +{ + "bot": { + "name": "What?", + "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/)\n**Discord.py:** [{_discord_v}](https://github.com/Rapptz/discord.py/tree/rewrite)\n**Lavalink:** [{_lavalink_v}](https://github.com/Devoxin/Lavalink.py)", + "stattext": "**Guilds:** {_guilds}\n**Users:** {_members}", + "what": "What?", + "how": "How?", + "how_many": "How many?", + "how_long": "How long?", + "footer_text": "Made by /r/Norge, for /r/Norge" + }, + "music": { + "title": "Music info", + "players": "Players", + "listeners": "Listeners" + } +} \ No newline at end of file diff --git a/localization/en_en/music.json b/localization/en_en/music.json new file mode 100644 index 0000000..2dc8c93 --- /dev/null +++ b/localization/en_en/music.json @@ -0,0 +1,79 @@ +{ + "response": { + "nothing_found": "Nothing found!", + "not_playing": "Not playing.", + "nothing_playing": "Nothing playing", + "have_to_listen": "You have to be listening to the bot", + "playlist_enqued": "Playlist Enqueued!", + "requested_by": "Requested by", + "tracks": "tracks", + "seek": { + "missing_amount": "You need to specify the amount of seconds to skip!", + "track_moved": "Moved track to **{_position}**" + }, + "skip": { + "skipped": "Skipped.", + "require_vote": "{_skips} out of {_total} required haters have voted to skip." + }, + "skip_to": { + "invalid_pos": "Invalid specified position.", + "exceeds_queue": "The position exceeds the queue\\'s length.", + "skipped_to": "skipped to `{_title}` at position `{_pos}`" + }, + "stop": "⏹ | Stopped.", + "live": "🔴 LIVE", + "now": "Now Playing", + "queue": { + "empty": "The queue is empty", + "user_empty": "{_user}'s queue is empty" + }, + "my_queue": "Your queue is empty", + "resume": { + "resumed": "⏯ | Resumed", + "paused": "⏯ | Paused" + }, + "shuffle": "Your queue has been shuffled", + "out_of_range": "Positions must be between 1 and {_len}", + "move": { + "not_in_queue": "Check that the positions exist in your queue", + "moved_to": "{_title} moved from position {_from} to {_to} in your queue" + }, + "remove": "**{_title}** removed.", + "dj_removed": "**{_title}** queued by {_user} removed.", + "dj_remove_user": "Removed all songs queued by <@{_id}>.", + "search": "searching", + "results": "Results", + "time_expired": "Timer Expired", + "disconnect": { + "not_in_voice": "You\\'re not in my voicechannel!", + "disconnected": "Disconnected" + }, + "not_connected": "Not connected.", + "volume": { + "set_to": "Volume set to {_volume}", + "reset": "Volume and equalizer reset", + "out_of_range": "you can only set the volume between 50 and 125" + }, + "boost": { + "on": "Bass boost is on", + "off": "'Bass boost is off" + }, + "scrub": { + "add": "Adding controls", + "controls": "{music_controls}", + "_comment_controls": "scrub.controls read from music_controls.txt" + }, + "enqueue": { + "position": "position", + "playing_in": "Playing in", + "enqueued": "Song enqueued", + "estimated": "estimated" + }, + "history": { + "title": "Track history", + "current": "`Current` **[{_title}]({_uri})** _by <@{_id}>_", + "track": "`{_index}.` **[{_title}]({_uri})** _by <@{_id}>_", + "previous": "**{_len} Previous songs:**" + } + } +} \ No newline at end of file diff --git a/localization/en_en/music_controls.txt b/localization/en_en/music_controls.txt new file mode 100644 index 0000000..88c47c3 --- /dev/null +++ b/localization/en_en/music_controls.txt @@ -0,0 +1,4 @@ +**Controls** +Play/pause: ▶ ⏸ +skip 15 seconds forward/backward: ⏪ ⏩ +Skip to beginning/end of song: ⏮ ⏭ \ No newline at end of file diff --git a/localization/global/vars.json b/localization/global/vars.json new file mode 100644 index 0000000..d2ab2e3 --- /dev/null +++ b/localization/global/vars.json @@ -0,0 +1,3 @@ +{ + "repo": "https://gitlab.com/Ev-1/shite-music-bot" +} \ No newline at end of file diff --git a/localization/nb_no/commands.json b/localization/nb_no/commands.json new file mode 100644 index 0000000..ee4ff4a --- /dev/null +++ b/localization/nb_no/commands.json @@ -0,0 +1,3 @@ +{ + "not_available_dm": "Denne kommandoen er ikke tilgjengelig i DMs" +} \ No newline at end of file diff --git a/localization/nb_no/info.json b/localization/nb_no/info.json new file mode 100644 index 0000000..ee6a065 --- /dev/null +++ b/localization/nb_no/info.json @@ -0,0 +1,18 @@ +{ + "bot": { + "name": "hva?", + "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/)\n**Discord.py:** [{_discord_v}](https://github.com/Rapptz/discord.py/tree/rewrite)\n**Lavalink:** [{_lavalink_v}](https://github.com/Devoxin/Lavalink.py)", + "stattext": "**Tenarar:** {_guilds}\n**Brukere:** {_members}", + "what": "Hva?", + "how": "Hvordan?", + "how_many": "Hvor mange?", + "how_long": "Hvor lenge?", + "footer_text": "Laget av /r/Norge, for /r/Norge" + }, + "music": { + "title": "Musikk info", + "players": "Spillere", + "listeners": "Lyttere" + } +} \ No newline at end of file diff --git a/localization/nb_no/music.json b/localization/nb_no/music.json new file mode 100644 index 0000000..72ab78a --- /dev/null +++ b/localization/nb_no/music.json @@ -0,0 +1,79 @@ +{ + "response": { + "nothing_found": "Kunne ikke finne sangen!", + "not_playing": "Spiller ikke av noe.", + "nothing_playing": "Ingenting spiller av", + "have_to_listen": "Du må høre på botten", + "playlist_enqued": "Spilleliste ble lagt til i køen!", + "requested_by": "Etterspurt av", + "tracks": "spor", + "seek": { + "missing_amount": "Du må spesifisere antall sekunder å spole!", + "track_moved": "Flyttet spor til **{_position}**" + }, + "skip": { + "skipped": "Hoppet over.", + "require_vote": "{_skips} av {_total} hatere må stemme for å hoppe over." + }, + "skip_to": { + "invalid_pos": "Ugyldig køposisjon.", + "exceeds_queue": "Posisjonen overgår køens lengde.", + "skipped_to": "hoppet til `{_title}` i posisjon `{_pos}`" + }, + "stop": "⏹ | Stoppet avspillingen.", + "live": "🔴 DIREKTE", + "now": "Spiller nå", + "queue": { + "empty": "Køen er tom", + "user_empty": "{_user} sin kø er tom" + }, + "my_queue": "Din kø er tom", + "resume": { + "resumed": "⏯ | Avspilling gjenopptatt", + "paused": "⏯ | Pauset avspillingen" + }, + "shuffle": "Køen din har blitt omstokket", + "out_of_range": "Posisjonene må være mellom 1 og {_len}", + "move": { + "not_in_queue": "Sjekk at posisjonen eksisterer i køen din", + "moved_to": "`{_title}` flyttet fra plass `{_from}` til `{_to}` i køen din" + }, + "remove": "**{_title}** ble fjernet fra køen.", + "dj_removed": "**{_title}** som ble lagt til i køen av {_user}, ble fjernet.", + "dj_remove_user": "Fjernet alle sanger lagt til i køen av <@{_id}>.", + "search": "søker", + "results": "Resultater", + "time_expired": "Tid utgått", + "disconnect": { + "not_in_voice": "Du må være koblet til samme talekanal som meg!", + "disconnected": "Frakoblet" + }, + "not_connected": "Du er ikke koblet til en talekanal.", + "volume": { + "set_to": "Volum satt til {_volume}", + "reset": "Volum og equalizer resatt!", + "out_of_range": "Du må sette volumet til et tall mellom 50 og 125" + }, + "boost": { + "on": "Bassforsterkeren er på", + "off": "Bassforsterkeren er av" + }, + "scrub": { + "add": "Legger til kontrollere", + "controls": "{music_controls}", + "_comment_controls": "scrub.controls read from music_controls.txt" + }, + "enqueue": { + "position": "Køposisjon", + "playing_in": "Spiller av om", + "enqueued": "Ny sang lagt til i køen", + "estimated": "estimert" + }, + "history": { + "title": "Avspillingshistorikk", + "current": "`Spiller nå` **[{_title}]({_uri})** _av <@{_id}>_", + "track": "`{_index}.` **[{_title}]({_uri})** _av <@{_id}>_", + "previous": "**Tidligere sanger ({_len}):**" + } + } +} \ No newline at end of file diff --git a/localization/nb_no/music_controls.txt b/localization/nb_no/music_controls.txt new file mode 100644 index 0000000..c625045 --- /dev/null +++ b/localization/nb_no/music_controls.txt @@ -0,0 +1,4 @@ +**Kontroller** +Spill/pause: ▶ ⏸ +Hopp 15 sekunder fremover/bakover: ⏪ ⏩ +Hopp til starten/slutten av sangen: ⏮ ⏭ \ No newline at end of file diff --git a/localization/nn_no/commands.json b/localization/nn_no/commands.json new file mode 100644 index 0000000..1a2c83c --- /dev/null +++ b/localization/nn_no/commands.json @@ -0,0 +1,3 @@ +{ + "not_available_dm": "Denna kommandoa er ikkje tilgjengeleg i direkte meldingar DMs" +} \ No newline at end of file diff --git a/localization/nn_no/info.json b/localization/nn_no/info.json new file mode 100644 index 0000000..ade527f --- /dev/null +++ b/localization/nn_no/info.json @@ -0,0 +1,18 @@ +{ + "bot": { + "name": "Kva?", + "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/)\n**Discord.py:** [{_discord_v}](https://github.com/Rapptz/discord.py/tree/rewrite)\n**Lavalink:** [{_lavalink_v}](https://github.com/Devoxin/Lavalink.py)", + "stattext": "**Tenarar:** {_guilds}\n**Brukarar:** {_members}", + "what": "Kva?", + "how": "Korleis?", + "how_many": "Kor mange?", + "how_long": "Kor lengje?", + "footer_text": "Laga av /r/Norge, for /r/Norge" + }, + "music": { + "title": "Musikk informasjon", + "players": "Spelarar", + "listeners": "Lyttarar" + } +} \ No newline at end of file diff --git a/localization/nn_no/music.json b/localization/nn_no/music.json new file mode 100644 index 0000000..7a469d5 --- /dev/null +++ b/localization/nn_no/music.json @@ -0,0 +1,79 @@ +{ + "response": { + "nothing_found": "Orsak, kunne ikkje finna songen!", + "not_playing": "Spelar ikkje noko.", + "nothing_playing": "Ingenting speles av", + "have_to_listen": "Dykk må høyra på botten", + "playlist_enqued": "Spellista vart lagt til i køen!", + "requested_by": "Etterspurd av", + "tracks": "spor", + "seek": { + "missing_amount": "Dykk må klargjera antal sekundar å spola!", + "track_moved": "Flytta spor til **{_position}**" + }, + "skip": { + "skipped": "Hoppa over.", + "require_vote": "{_skips} av {_total} hatarar må stemma for å hoppa over." + }, + "skip_to": { + "invalid_pos": "Ugild køposisjon.", + "exceeds_queue": "Posisjona overgår køas lengde.", + "skipped_to": "hoppa til `{_title}` i posisjon `{_pos}`" + }, + "stop": "⏹ | Stoppa avspelinga.", + "live": "🔴 DIREKTE", + "now": "Spelar no", + "queue": { + "empty": "Køa er tom", + "user_empty": "{_user} sin kø er tom" + }, + "my_queue": "Din kø er tom", + "resume": { + "resumed": "⏯ | Avspeling oppattatt", + "paused": "⏯ | Pauset avspeling" + }, + "shuffle": "Køa di har vorte omstokka", + "out_of_range": "Posisjonane må væra millom 1 og {_len}", + "move": { + "not_in_queue": "Kontrollar at posisjona eksisterar i køa di", + "moved_to": "`{_title}` flytta frå plass `{_from}` til `{_to}` i køa di" + }, + "remove": "**{_title}** vart fjerna frå køa.", + "dj_removed": "**{_title}** som vart lagt til i køa av {_user}, vart fjerna.", + "dj_remove_user": "Fjerna alle songar lagt til i køa av <@{_id}>.", + "search": "søkjar", + "results": "Resultatar", + "time_expired": "Tid utgått", + "disconnect": { + "not_in_voice": "Dykk må være tilkopla same talekanal som meg!", + "disconnected": "Fråkopla" + }, + "not_connected": "Dykk er ikkje kopla til ein talekanal.", + "volume": { + "set_to": "Volum satt til {_volume}", + "reset": "Volum og utjevnar igjensatt!", + "out_of_range": "Du må setja volumet til eit tal mellom femti og hundreogtjuefem" + }, + "boost": { + "on": "Bassforsterkaren er på", + "off": "Bassforsterkaren er av" + }, + "scrub": { + "add": "Legjar til kontrollera", + "controls": "{music_controls}", + "_comment_controls": "scrub.controls read from music_controls.txt" + }, + "enqueue": { + "position": "Køposisjon", + "playing_in": "Spelar av om", + "enqueued": "Ny song lagt til i køa", + "estimated": "estimert" + }, + "history": { + "title": "Avspelingshistorikk", + "current": "`Spelar no` **[{_title}]({_uri})** _av <@{_id}>_", + "track": "`{_index}.` **[{_title}]({_uri})** _av <@{_id}>_", + "previous": "**Tidlegare songar ({_len}):**" + } + } +} \ No newline at end of file diff --git a/localization/nn_no/music_controls.txt b/localization/nn_no/music_controls.txt new file mode 100644 index 0000000..225d830 --- /dev/null +++ b/localization/nn_no/music_controls.txt @@ -0,0 +1,4 @@ +**Kontrollar** +Spel/pause: ▶ ⏸ +Hopp 15 sekundar framover/bakover: ⏪ ⏩ +Hopp til byrjinga/slutten av songen: ⏮ ⏭ \ No newline at end of file