From 9fb92c449cf04804f424751efd71e06b3ac60a6e Mon Sep 17 00:00:00 2001 From: qwewqa <198e559dbd446d973355f415bdfa34@gmail.com> Date: Mon, 11 Jan 2021 12:18:28 -0500 Subject: [PATCH] minor fixes --- miyu_bot/commands/cogs/event.py | 57 +++++++++++++++---- miyu_bot/commands/cogs/music.py | 45 +++++++++++---- miyu_bot/commands/common/fuzzy_matching.py | 4 +- .../commands/common/master_asset_manager.py | 25 +++++--- miyu_bot/commands/common/reaction_message.py | 10 +++- 5 files changed, 106 insertions(+), 35 deletions(-) diff --git a/miyu_bot/commands/cogs/event.py b/miyu_bot/commands/cogs/event.py index 1a44605..a12cea4 100644 --- a/miyu_bot/commands/cogs/event.py +++ b/miyu_bot/commands/cogs/event.py @@ -1,16 +1,18 @@ -import datetime +import datetime as dt import logging import discord +import pytz from d4dj_utils.master.event_master import EventMaster, EventState from discord.ext import commands +from pytz import UnknownTimeZoneError from main import asset_manager from miyu_bot.commands.common.emoji import attribute_emoji_ids_by_attribute_id, unit_emoji_ids_by_unit_id, \ parameter_bonus_emoji_ids_by_parameter_id, \ event_point_emoji_id from miyu_bot.commands.common.formatting import format_info -from miyu_bot.commands.common.fuzzy_matching import FuzzyFilteredMap, romanize +from miyu_bot.commands.common.fuzzy_matching import romanize from miyu_bot.commands.common.master_asset_manager import MasterAssetManager @@ -21,7 +23,8 @@ class Event(commands.Cog): self.events = MasterAssetManager( asset_manager.event_master, naming_function=lambda e: e.name, - filter_function=lambda e: e.start_datetime < datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(hours=8), + filter_function=lambda e: e.start_datetime < dt.datetime.now( + dt.timezone.utc) + dt.timedelta(hours=12), ) @commands.command(name='event', @@ -44,7 +47,11 @@ class Event(commands.Cog): return self.logger.info(f'Found event "{event}" ({romanize(event.name)}).') - logo = discord.File(event.logo_path, filename='logo.png') + try: + logo = discord.File(event.logo_path, filename='logo.png') + except FileNotFoundError: + # Just a fallback + logo = discord.File(self.events.get('1', ctx).logo_path, filename='logo.png') embed = discord.Embed(title=event.name) embed.set_thumbnail(url=f'attachment://logo.png') @@ -98,6 +105,29 @@ class Event(commands.Cog): await ctx.send(files=[logo], embed=embed) + @commands.command(name='time', + aliases=[], + description='Displays the current time', + help='!time') + async def time(self, ctx: commands.Context, *, arg=''): + embed = discord.Embed(title='Time') + + def format_time(t: dt.datetime): + return str(t.replace(microsecond=0)) + + embed.add_field(name='Asia/Tokyo', value=format_time(dt.datetime.now(pytz.timezone('Asia/Tokyo'))), inline=False) + + if arg: + try: + embed.add_field(name=arg, value=format_time(dt.datetime.now(pytz.timezone(arg))), inline=False) + except UnknownTimeZoneError: + await ctx.send(content=f'Invalid timezone "{arg}".') + return + else: + embed.add_field(name='UTC', value=format_time(dt.datetime.now(dt.timezone.utc)), inline=False) + + await ctx.send(embed=embed) + @commands.command(name='timeleft', aliases=['tl', 'time_left'], description='Displays the time left in the current event', @@ -116,28 +146,28 @@ class Event(commands.Cog): if state == EventState.Upcoming: time_delta_heading = 'Time Until Start' - time_delta = latest.start_datetime - datetime.datetime.now(datetime.timezone.utc) + time_delta = latest.start_datetime - dt.datetime.now(dt.timezone.utc) date_heading = 'Start Date' date_value = latest.start_datetime elif state == EventState.Open: time_delta_heading = 'Time Until Close' - time_delta = latest.reception_close_datetime - datetime.datetime.now(datetime.timezone.utc) + time_delta = latest.reception_close_datetime - dt.datetime.now(dt.timezone.utc) progress = 1 - (time_delta / (latest.reception_close_datetime - latest.start_datetime)) date_heading = 'Close Date' date_value = latest.reception_close_datetime elif state in (EventState.Closing, EventState.Ranks_Fixed): time_delta_heading = 'Time Until Results' - time_delta = latest.result_announcement_datetime - datetime.datetime.now(datetime.timezone.utc) + time_delta = latest.result_announcement_datetime - dt.datetime.now(dt.timezone.utc) date_heading = 'Results Date' date_value = latest.result_announcement_datetime elif state == EventState.Results: time_delta_heading = 'Time Until End' - time_delta = latest.end_datetime - datetime.datetime.now(datetime.timezone.utc) + time_delta = latest.end_datetime - dt.datetime.now(dt.timezone.utc) date_heading = 'End Date' date_value = latest.end_datetime else: time_delta_heading = 'Time Since End' - time_delta = datetime.datetime.now(datetime.timezone.utc) - latest.end_datetime + time_delta = dt.datetime.now(dt.timezone.utc) - latest.end_datetime date_heading = 'End Date' date_value = latest.end_datetime @@ -160,10 +190,15 @@ class Event(commands.Cog): def get_latest_event(self, ctx: commands.Context) -> EventMaster: """Returns the oldest event that has not ended or the newest event otherwise.""" try: - return min((v for v in self.events.values(ctx) if v.state() < EventState.Ended), + # NY event overlapped with previous event + return min((v for v in self.events.values(ctx) if v.state() == EventState.Open), key=lambda e: e.start_datetime) except ValueError: - return max(self.events.values(ctx), key=lambda v: v.start_datetime) + try: + return min((v for v in self.events.values(ctx) if v.state() < EventState.Ended), + key=lambda e: e.start_datetime) + except ValueError: + return max(self.events.values(ctx), key=lambda v: v.start_datetime) def setup(bot): diff --git a/miyu_bot/commands/cogs/music.py b/miyu_bot/commands/cogs/music.py index 100e374..2aee918 100644 --- a/miyu_bot/commands/cogs/music.py +++ b/miyu_bot/commands/cogs/music.py @@ -26,6 +26,7 @@ class Music(commands.Cog): asset_manager.music_master, naming_function=lambda m: f'{m.name} {m.special_unit_name}', filter_function=lambda m: m.is_released, + fallback_naming_function=lambda m: m.id, ) @property @@ -63,7 +64,11 @@ class Music(commands.Cog): return self.logger.info(f'Found song "{song}" ({romanize(song.name)}).') - thumb = discord.File(song.jacket_path, filename='jacket.png') + try: + thumb = discord.File(song.jacket_path, filename='jacket.png') + except FileNotFoundError: + # dig delight is just a fallback + thumb = discord.File(self.music.get('110001', ctx).jacket_path, filename='jacket.png') embed = discord.Embed(title=song.name) embed.set_thumbnail(url=f'attachment://jacket.png') @@ -150,26 +155,42 @@ class Music(commands.Cog): aliases=['songsearch', 'song_search'], description='Finds songs matching the given name.', help='!songs grgr') - async def songs(self, ctx: commands.Context, *, arg: str = ""): - if arg: - self.logger.info(f'Searching for songs "{arg}".') - songs = self.music.get_sorted(arg, ctx) + async def songs(self, ctx: commands.Context, *, arg: str = ''): + self.logger.info(f'Searching for songs "{arg}".' if arg else 'Listing songs.') + sort = 'relevance' + if not arg: + sort = 'default' + elif arg == 'duration': + sort = 'duration' + arg = '' + songs = self.music.get_sorted(arg, ctx) + if sort == 'relevance': listing = [f'{song.name}{" (" + song.special_unit_name + ")" if song.special_unit_name else ""}' for song in songs] - asyncio.ensure_future(run_paged_message(ctx, f'Song Search "{arg}"', listing)) + elif sort == 'duration': + songs = sorted(songs, key=lambda s: self.get_music_duration(s)) + listing = [ + f'{self.format_duration(self.get_music_duration(song))} {song.name}{" (" + song.special_unit_name + ")" if song.special_unit_name else ""}' + for song in songs] else: - self.logger.info('Listing songs.') - songs = sorted(self.music.values(ctx), key=lambda m: -m.default_order) - songs = [*songs[1:], songs[0]] # lesson is always first + songs = sorted(songs, key=lambda s: -s.default_order) listing = [f'{song.name}{" (" + song.special_unit_name + ")" if song.special_unit_name else ""}' for song in - songs] - asyncio.ensure_future(run_paged_message(ctx, f'All Songs', listing)) + [*songs[1:], songs[0]]] # lesson is always first + asyncio.ensure_future(run_paged_message(ctx, f'Song Search "{arg}"' if arg else 'Songs', listing)) return def get_chart_embed_info(self, song): embeds = [] - files = [discord.File(song.jacket_path, filename=f'jacket.png')] + + try: + thumb = discord.File(song.jacket_path, filename='jacket.png') + except FileNotFoundError: + # dig delight is just a fallback + thumb = discord.File(self.music.get('110001', None).jacket_path, filename='jacket.png') + + files = [thumb] + for difficulty in [ChartDifficulty.Easy, ChartDifficulty.Normal, ChartDifficulty.Hard, ChartDifficulty.Expert]: chart = song.charts[difficulty] embed = discord.Embed(title=f'{song.name} [{chart.difficulty.name}]') diff --git a/miyu_bot/commands/common/fuzzy_matching.py b/miyu_bot/commands/common/fuzzy_matching.py index 6070ae4..aeabac3 100644 --- a/miyu_bot/commands/common/fuzzy_matching.py +++ b/miyu_bot/commands/common/fuzzy_matching.py @@ -107,6 +107,7 @@ class FuzzyMatchConfig: ('v', 'b'): 0.0, ('l', 'r'): 0.0, ('c', 'k'): 0.0, + ('y', 'i'): 0.4, }) word_match_weight: float = -0.2 whole_match_weight: float = -0.25 @@ -129,7 +130,8 @@ class FuzzyMatcher: self.array[0][i] = i * self.config.insertion_weight def score(self, source: str, target: str, threshold=0.0): - # target must not be empty + if not target: + return 1 l_src = len(source) l_tgt = len(target) diff --git a/miyu_bot/commands/common/master_asset_manager.py b/miyu_bot/commands/common/master_asset_manager.py index 18f8148..ecc3544 100644 --- a/miyu_bot/commands/common/master_asset_manager.py +++ b/miyu_bot/commands/common/master_asset_manager.py @@ -1,4 +1,4 @@ -from typing import Callable, Any +from typing import Callable, Any, Optional from d4dj_utils.master.master_asset import MasterDict from discord.ext import commands @@ -7,19 +7,22 @@ from miyu_bot.commands.common.fuzzy_matching import FuzzyFilteredMap class MasterAssetManager: - def __init__(self, masters: MasterDict, naming_function: Callable[[Any], str], filter_function=lambda _: True): + def __init__(self, masters: MasterDict, naming_function: Callable[[Any], str], filter_function=lambda _: True, + fallback_naming_function: Optional[Callable[[Any], str]] = None): self.masters = masters self.fuzzy_map = FuzzyFilteredMap(filter_function) self.unfiltered_fuzzy_map = FuzzyFilteredMap() for master in masters.values(): name = naming_function(master) + if self.fuzzy_map.has_exact(name) and fallback_naming_function: + name = fallback_naming_function(master) if self.fuzzy_map.has_exact(name): continue self.fuzzy_map[name] = master self.unfiltered_fuzzy_map[name] = master - def get(self, name_or_id: str, ctx: commands.Context): - if ctx.channel.id in no_filter_channels: + def get(self, name_or_id: str, ctx: Optional[commands.Context]): + if ctx and ctx.channel.id in no_filter_channels: try: return self.masters[int(name_or_id)] except (KeyError, ValueError): @@ -34,10 +37,16 @@ class MasterAssetManager: return self.fuzzy_map[name_or_id] def get_sorted(self, name: str, ctx: commands.Context): - if ctx.channel.id in no_filter_channels: - return self.unfiltered_fuzzy_map.get_sorted(name) + if name: + if ctx.channel.id in no_filter_channels: + return self.unfiltered_fuzzy_map.get_sorted(name) + else: + return self.fuzzy_map.get_sorted(name) else: - return self.fuzzy_map.get_sorted(name) + if ctx.channel.id in no_filter_channels: + return list(self.unfiltered_fuzzy_map.values()) + else: + return list(self.fuzzy_map.values()) def values(self, ctx: commands.Context): if ctx.channel.id in no_filter_channels: @@ -46,4 +55,4 @@ class MasterAssetManager: return self.fuzzy_map.values() -no_filter_channels = {790033228600705048, 790033272376918027} +no_filter_channels = {790033228600705048, 790033272376918027, 795640603114864640} diff --git a/miyu_bot/commands/common/reaction_message.py b/miyu_bot/commands/common/reaction_message.py index c4b0b7b..4f46e7f 100644 --- a/miyu_bot/commands/common/reaction_message.py +++ b/miyu_bot/commands/common/reaction_message.py @@ -15,7 +15,7 @@ async def run_tabbed_message(ctx: Context, message: Message, emojis: List[Emoji] await run_reaction_message(ctx, message, emojis, callback, timeout) -async def run_paged_message(ctx: Context, title: str, content: List[str], page_size: int = 15, +async def run_paged_message(ctx: Context, title: str, content: List[str], page_size: int = 15, numbered: bool = True, timeout=300, double_arrow_threshold=4): if not content: embed = discord.Embed(title=title).set_footer(text='Page 0/0') @@ -25,14 +25,18 @@ async def run_paged_message(ctx: Context, title: str, content: List[str], page_s page_contents = [content[i:i + page_size] for i in range(0, len(content), page_size)] item_number = 0 + max_item_number_length = len(str(len(content))) def format_item(item): nonlocal item_number item_number += 1 - return f'{item_number}. {item}' + if numbered: + return f'{item_number}.{" " * (max_item_number_length - len(str(item_number)))} {item}' + else: + return str(item) embeds = [ - discord.Embed(title=title, description='\n'.join((format_item(i) for i in page))).set_footer( + discord.Embed(title=title, description='```' + '\n'.join((format_item(i) for i in page)) + '```').set_footer( text=f'Page {i + 1}/{len(page_contents)}') for i, page in enumerate(page_contents)]