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