init
This commit is contained in:
commit
3cb092abb9
251
.gitignore
vendored
Normal file
251
.gitignore
vendored
Normal file
@ -0,0 +1,251 @@
|
||||
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/pycharm,python
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=pycharm,python
|
||||
|
||||
### PyCharm ###
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
|
||||
# Generated files
|
||||
.idea/**/contentModel.xml
|
||||
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
|
||||
# Gradle
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# Gradle and Maven with auto-import
|
||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||
# since they will be recreated, and may cause churn. Uncomment if using
|
||||
# auto-import.
|
||||
# .idea/artifacts
|
||||
# .idea/compiler.xml
|
||||
# .idea/jarRepositories.xml
|
||||
# .idea/modules.xml
|
||||
# .idea/*.iml
|
||||
# .idea/modules
|
||||
# *.iml
|
||||
# *.ipr
|
||||
|
||||
# CMake
|
||||
cmake-build-*/
|
||||
|
||||
# Mongo Explorer plugin
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
# File-based project format
|
||||
*.iws
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
# Editor-based Rest Client
|
||||
.idea/httpRequests
|
||||
|
||||
# Android studio 3.1+ serialized cache file
|
||||
.idea/caches/build_file_checksums.ser
|
||||
|
||||
### PyCharm Patch ###
|
||||
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
|
||||
|
||||
# *.iml
|
||||
# modules.xml
|
||||
# .idea/misc.xml
|
||||
# *.ipr
|
||||
|
||||
# Sonarlint plugin
|
||||
# https://plugins.jetbrains.com/plugin/7973-sonarlint
|
||||
.idea/**/sonarlint/
|
||||
|
||||
# SonarQube Plugin
|
||||
# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
|
||||
.idea/**/sonarIssues.xml
|
||||
|
||||
# Markdown Navigator plugin
|
||||
# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
|
||||
.idea/**/markdown-navigator.xml
|
||||
.idea/**/markdown-navigator-enh.xml
|
||||
.idea/**/markdown-navigator/
|
||||
|
||||
# Cache file creation bug
|
||||
# See https://youtrack.jetbrains.com/issue/JBR-2257
|
||||
.idea/$CACHE_FILE$
|
||||
|
||||
# CodeStream plugin
|
||||
# https://plugins.jetbrains.com/plugin/12206-codestream
|
||||
.idea/codestream.xml
|
||||
|
||||
### Python ###
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
pip-wheel-metadata/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
pytestdebug.log
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
doc/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
pythonenv*
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# profiling data
|
||||
.prof
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/pycharm,python
|
||||
|
||||
config.json
|
||||
assets/
|
23
main.py
Normal file
23
main.py
Normal file
@ -0,0 +1,23 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
import discord
|
||||
from d4dj_utils.manager.asset_manager import AssetManager
|
||||
from discord.ext import commands
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
with open('config.json') as f:
|
||||
bot_token = json.load(f)['token']
|
||||
bot = commands.Bot(command_prefix='!', case_insensitive=True)
|
||||
asset_manager = AssetManager('assets')
|
||||
bot.load_extension('miyu_bot.commands.cogs.chart')
|
||||
|
||||
|
||||
@bot.event
|
||||
async def on_ready():
|
||||
logging.getLogger(__name__).info(f'Current server count: {len(bot.guilds)}')
|
||||
await bot.change_presence(activity=discord.Game(name='test'))
|
||||
|
||||
|
||||
bot.run(bot_token)
|
106
miyu_bot/commands/cogs/chart.py
Normal file
106
miyu_bot/commands/cogs/chart.py
Normal file
@ -0,0 +1,106 @@
|
||||
import logging
|
||||
|
||||
import discord
|
||||
from d4dj_utils.master.chart_master import ChartDifficulty, ChartMaster
|
||||
from d4dj_utils.master.common_enums import ChartSectionType
|
||||
from d4dj_utils.master.music_master import MusicMaster
|
||||
from discord.ext import commands
|
||||
|
||||
from main import asset_manager
|
||||
from miyu_bot.commands.common.fuzzy_matching import romanize, FuzzyMatcher
|
||||
|
||||
|
||||
class Charts(commands.Cog):
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self.music = self.get_music()
|
||||
|
||||
def get_music(self):
|
||||
music = FuzzyMatcher(lambda m: m.is_released)
|
||||
for m in asset_manager.music_master.values():
|
||||
music[f'{m.name} {m.special_unit_name}'] = m
|
||||
return music
|
||||
|
||||
difficulty_names = {
|
||||
'expert': ChartDifficulty.Expert,
|
||||
'hard': ChartDifficulty.Hard,
|
||||
'normal': ChartDifficulty.Normal,
|
||||
'easy': ChartDifficulty.Easy,
|
||||
'exp': ChartDifficulty.Expert,
|
||||
'hrd': ChartDifficulty.Hard,
|
||||
'nrm': ChartDifficulty.Normal,
|
||||
'esy': ChartDifficulty.Easy,
|
||||
'ex': ChartDifficulty.Expert,
|
||||
'hd': ChartDifficulty.Hard,
|
||||
'nm': ChartDifficulty.Normal,
|
||||
'es': ChartDifficulty.Easy,
|
||||
}
|
||||
|
||||
@commands.command()
|
||||
async def chart(self, ctx, *, arg):
|
||||
self.logger.info(f'Searching for chart "{arg}".')
|
||||
|
||||
arg = arg.strip()
|
||||
|
||||
if not arg:
|
||||
await ctx.send('Argument is empty.')
|
||||
return
|
||||
|
||||
split_args = arg.split()
|
||||
|
||||
difficulty = ChartDifficulty.Expert
|
||||
if len(split_args) >= 2:
|
||||
final_word = split_args[-1]
|
||||
if final_word in self.difficulty_names:
|
||||
difficulty = self.difficulty_names[final_word]
|
||||
arg = ''.join(split_args[:-1])
|
||||
|
||||
song: MusicMaster = self.music[arg]
|
||||
if not song:
|
||||
msg = f'Failed to find chart "{arg}".'
|
||||
await ctx.send(msg)
|
||||
self.logger.info(msg)
|
||||
return
|
||||
self.logger.info(f'Found "{song}" ({romanize(song.name)[1]}).')
|
||||
|
||||
chart: ChartMaster = song.charts[difficulty]
|
||||
|
||||
chart_data = chart.load_chart_data()
|
||||
note_counts = chart_data.get_note_counts()
|
||||
|
||||
thumb = discord.File(song.jacket_path, filename='jacket.png')
|
||||
render = discord.File(chart.image_path, filename='render.png')
|
||||
|
||||
embed = discord.Embed(title=song.name)
|
||||
embed.set_thumbnail(url=f'attachment://jacket.png')
|
||||
embed.set_image(url=f'attachment://render.png')
|
||||
|
||||
embed.add_field(name='Info',
|
||||
value=f'Difficulty: {chart.display_level} ({chart.difficulty.name})\n'
|
||||
f'Unit: {song.special_unit_name or song.unit.name}\n'
|
||||
f'Category: {song.category.name}\n'
|
||||
f'BPM: {song.bpm}',
|
||||
inline=False)
|
||||
embed.add_field(name='Combo',
|
||||
value=f'Max Combo: {chart.note_counts[ChartSectionType.Full].count}\n'
|
||||
f'Taps: {note_counts["tap"]} (dark: {note_counts["tap1"]}, light: {note_counts["tap2"]})\n'
|
||||
f'Scratches: {note_counts["scratch"]} (left: {note_counts["scratch_left"]}, right: {note_counts["scratch_right"]})\n'
|
||||
f'Stops: {note_counts["stop"]} (head: {note_counts["stop_start"]}, tail: {note_counts["stop_end"]})\n'
|
||||
f'Long: {note_counts["long"]} (head: {note_counts["long_start"]}, tail: {note_counts["long_end"]})\n'
|
||||
f'Slide: {note_counts["slide"]} (tick: {note_counts["slide_tick"]}, flick {note_counts["slide_flick"]})',
|
||||
inline=True)
|
||||
embed.add_field(name='Ratings',
|
||||
value=f'NTS: {round(chart.trends[0] * 100, 2)}%\n'
|
||||
f'DNG: {round(chart.trends[1] * 100, 2)}%\n'
|
||||
f'SCR: {round(chart.trends[2] * 100, 2)}%\n'
|
||||
f'EFT: {round(chart.trends[3] * 100, 2)}%\n'
|
||||
f'TEC: {round(chart.trends[4] * 100, 2)}%\n',
|
||||
inline=True
|
||||
)
|
||||
|
||||
await ctx.send(files=[thumb, render], embed=embed)
|
||||
|
||||
|
||||
def setup(bot):
|
||||
bot.add_cog(Charts(bot))
|
76
miyu_bot/commands/common/fuzzy_matching.py
Normal file
76
miyu_bot/commands/common/fuzzy_matching.py
Normal file
@ -0,0 +1,76 @@
|
||||
import logging
|
||||
import re
|
||||
from typing import Tuple
|
||||
|
||||
import pykakasi
|
||||
|
||||
|
||||
class FuzzyMatcher:
|
||||
def __init__(self, filter, threshold: float = 1):
|
||||
self.filter = filter or (lambda n: True)
|
||||
self.threshold = threshold
|
||||
self.values = {}
|
||||
self.max_length = 0
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
k = romanize(key)
|
||||
self.values[k] = value
|
||||
self.max_length = len(k[0])
|
||||
|
||||
def __getitem__(self, key):
|
||||
if len(key) > self.max_length * 1.1:
|
||||
self.logger.debug(f'Rejected key "{key}" due to length.')
|
||||
return None
|
||||
key, _ = romanize(key)
|
||||
result = min((k for k, v in self.values.items() if self.filter(v)),
|
||||
key=lambda v: fuzzy_match_score(key, *v, threshold=self.threshold))
|
||||
if fuzzy_match_score(key, *result, threshold=self.threshold) > self.threshold:
|
||||
return None
|
||||
return self.values[result]
|
||||
|
||||
|
||||
_insertion_weight = 0.001
|
||||
_deletion_weight = 1
|
||||
_substitution_weight = 1
|
||||
|
||||
|
||||
def fuzzy_match_score(source: str, target: str, words, threshold: float) -> float:
|
||||
m = len(source)
|
||||
n = len(target)
|
||||
a = [[0] * (n + 1) for _ in range(m + 1)]
|
||||
|
||||
for i in range(m + 1):
|
||||
a[i][0] = i
|
||||
|
||||
for i in range(n + 1):
|
||||
a[0][i] = i * _insertion_weight
|
||||
|
||||
def strip_vowels(s):
|
||||
return re.sub('[aeoiu]', '', s)
|
||||
|
||||
word_match_bonus = 0.1 * max(max(sum(a == b for a, b in zip(source, w)) for w in words),
|
||||
max(sum(a == b for a, b in
|
||||
zip(source[0] + strip_vowels(source[1:]), w[0] + strip_vowels(w[1:]))) for w in
|
||||
words),
|
||||
sum(a == b for a, b in zip(source, ''.join(w[0] for w in words))))
|
||||
|
||||
for i in range(1, m + 1):
|
||||
for j in range(1, n + 1):
|
||||
a[i][j] = min(a[i - 1][j - 1] + _substitution_weight if source[i - 1] != target[j - 1] else a[i - 1][j - 1],
|
||||
a[i - 1][j] + _deletion_weight,
|
||||
a[i][j - 1] + _insertion_weight)
|
||||
if j == n and (a[i][j] - (m - i) * _insertion_weight - word_match_bonus) > threshold:
|
||||
return 9999
|
||||
|
||||
return a[m][n] - word_match_bonus
|
||||
|
||||
|
||||
def romanize(s: str) -> Tuple[str, Tuple[str]]:
|
||||
kks = pykakasi.kakasi()
|
||||
s = re.sub('[\']', '', s)
|
||||
s = re.sub('[A-Za-z]+', lambda ele: f' {ele[0]} ', s)
|
||||
s = ' '.join(c['hepburn'].strip().lower() for c in kks.convert(s))
|
||||
s = re.sub(r'[^a-zA-Z0-9_ ]+', '', s)
|
||||
words = tuple(s.split())
|
||||
return ''.join(words), words
|
19
update_assets.py
Normal file
19
update_assets.py
Normal file
@ -0,0 +1,19 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import logging.config
|
||||
|
||||
from d4dj_utils.manager.asset_manager import AssetManager
|
||||
from d4dj_utils.manager.revision_manager import RevisionManager
|
||||
|
||||
|
||||
async def main():
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
revision_manager = RevisionManager('assets')
|
||||
await revision_manager.repair_downloads()
|
||||
await revision_manager.update_assets()
|
||||
manager = AssetManager('assets')
|
||||
manager.render_charts()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
Loading…
x
Reference in New Issue
Block a user