├── .env.dev ├── .env.prod ├── .gitignore ├── LICENSE ├── README.md ├── bot.py ├── pyproject.toml ├── requirements.txt ├── src ├── libraries │ ├── GLOBAL_CONSTANT.py │ ├── GLOBAL_PATH.py │ ├── GLOBAL_RULE.py │ ├── data_handle │ │ ├── abstract_db_handle.py │ │ ├── alias_db_handle.py │ │ ├── black_list_handle.py │ │ └── userdata_handle.py │ ├── execution_time.py │ ├── image_handle │ │ └── image.py │ └── maimai │ │ ├── completion_status_table │ │ ├── new_pcst_base_img.py │ │ ├── user_level_completion_status_table.py │ │ ├── user_version_completion_status_table.py │ │ ├── utils.py │ │ └── version_image.py │ │ ├── maimai_fortune │ │ ├── maimai_fortune.py │ │ └── utils.py │ │ ├── maimai_music_data │ │ ├── maimai_music_data.py │ │ ├── maimai_utage_music_data.py │ │ └── utils.py │ │ ├── maimai_play_best50 │ │ ├── maimai_play_best50_new.py │ │ ├── maimai_play_best50_old_design.py │ │ ├── maimai_recommend.py │ │ └── utils.py │ │ ├── maimaidx_music.py │ │ ├── near_maimai.py │ │ ├── player_music_score │ │ ├── generate_utils.py │ │ └── player_music_score.py │ │ └── utils.py └── plugins │ ├── xray_plugin_black_list │ └── __init__.py │ ├── xray_plugin_guess_music │ └── __init__.py │ ├── xray_plugin_maimaidx │ ├── __init__.py │ └── utils.py │ ├── xray_plugin_utils │ └── __init__.py │ ├── xray_plugins_generate_completion_status_table │ ├── __init__.py │ └── common.py │ ├── xray_plugins_log │ └── __init__.py │ ├── xray_plugins_saveimage │ └── __init__.py │ └── xray_plugins_userdata │ └── __init__.py └── 重构命名规范.md /.env.dev: -------------------------------------------------------------------------------- 1 | HOST=0.0.0.0 2 | PORT=11113 3 | FASTAPI_RELOAD=False 4 | LOG_LEVEL=SUCCESS 5 | SUPERUSERS=[381268035] 6 | COMMAND_START=[""] -------------------------------------------------------------------------------- /.env.prod: -------------------------------------------------------------------------------- 1 | HOST=0.0.0.0 2 | PORT=11113 3 | FASTAPI_RELOAD=False 4 | LOG_LEVEL=SUCCESS 5 | SUPERUSERS=[381268035] 6 | COMMAND_START=[""] -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/python 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=python 4 | 5 | ### Python ### 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | pip-wheel-metadata/ 28 | share/python-wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | MANIFEST 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .nox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | *.py,cover 55 | .hypothesis/ 56 | .pytest_cache/ 57 | pytestdebug.log 58 | 59 | # Translations 60 | *.mo 61 | *.pot 62 | 63 | # Django stuff: 64 | *.log 65 | local_settings.py 66 | db.sqlite3 67 | db.sqlite3-journal 68 | 69 | # Flask stuff: 70 | instance/ 71 | .webassets-cache 72 | 73 | # Scrapy stuff: 74 | .scrapy 75 | 76 | # Sphinx documentation 77 | docs/_build/ 78 | doc/_build/ 79 | 80 | # PyBuilder 81 | target/ 82 | 83 | # Jupyter Notebook 84 | .ipynb_checkpoints 85 | 86 | # IPython 87 | profile_default/ 88 | ipython_config.py 89 | 90 | # pyenv 91 | .python-version 92 | 93 | # pipenv 94 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 95 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 96 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 97 | # install all needed dependencies. 98 | #Pipfile.lock 99 | 100 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 101 | __pypackages__/ 102 | 103 | # Celery stuff 104 | celerybeat-schedule 105 | celerybeat.pid 106 | 107 | # SageMath parsed files 108 | *.sage.py 109 | 110 | # Environments 111 | .env 112 | .venv 113 | env/ 114 | venv/ 115 | ENV/ 116 | env.bak/ 117 | venv.bak/ 118 | 119 | # Spyder project settings 120 | .spyderproject 121 | .spyproject 122 | 123 | # Rope project settings 124 | .ropeproject 125 | 126 | # mkdocs documentation 127 | /site 128 | 129 | # mypy 130 | .mypy_cache/ 131 | .dmypy.json 132 | dmypy.json 133 | 134 | # Pyre type checker 135 | .pyre/ 136 | 137 | # pytype static type analyzer 138 | .pytype/ 139 | 140 | # End of https://www.toptal.com/developers/gitignore/api/python 141 | 142 | data/ 143 | *.psd 144 | src/static/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xray_mai_bot_v2 2 | 3 | ## How to start 4 | 5 | 1. generate project using `nb create` . 6 | 2. create your plugin using `nb plugin create` . 7 | 3. writing your plugins under `src/plugins` folder. 8 | 4. run your bot using `nb run --reload` . 9 | 10 | ## Documentation 11 | 12 | See [Docs](https://nonebot.dev/) 13 | -------------------------------------------------------------------------------- /bot.py: -------------------------------------------------------------------------------- 1 | import nonebot 2 | from nonebot.adapters.onebot.v11 import Adapter as OneBotV11Adapter 3 | 4 | nonebot.init() 5 | app = nonebot.get_asgi() 6 | 7 | driver = nonebot.get_driver() 8 | driver.register_adapter(OneBotV11Adapter) 9 | 10 | nonebot.load_from_toml("pyproject.toml") 11 | 12 | 13 | if __name__ == "__main__": 14 | nonebot.logger.warning( 15 | "Always use `nb run` to start the bot instead of manually running!" 16 | ) 17 | 18 | nonebot.run(app="__mp_main__:app") 19 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "xray_mai_bot_v2" 3 | version = "0.1.0" 4 | description = "xray_mai_bot_v2" 5 | readme = "README.md" 6 | requires-python = ">=3.8, <4.0" 7 | 8 | [tool.nonebot] 9 | adapters = [ 10 | { name = "OneBot V11", module_name = "nonebot.adapters.onebot.v11" } 11 | ] 12 | plugins = [] 13 | plugin_dirs = ["src/plugins"] 14 | builtin_plugins = [] 15 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | nonebot2==2.2.1 2 | Pillow==9.5.0 3 | nb-cli==1.2.2 4 | pymongo==3.11.3 -------------------------------------------------------------------------------- /src/libraries/GLOBAL_CONSTANT.py: -------------------------------------------------------------------------------- 1 | MAIPLATE_FILE_ID_MAP = { 2 | '真極': '006101', 3 | '真将': '0066op', 4 | '真神': '006102', 5 | '真舞舞': '006103', 6 | '超極': '006104', 7 | '超将': '006105', 8 | '超神': '006106', 9 | '超舞舞': '006107', 10 | '檄極': '006108', 11 | '檄将': '006109', 12 | '檄神': '006110', 13 | '檄舞舞': '006111', 14 | '橙極': '006112', 15 | '橙将': '006113', 16 | '橙神': '006114', 17 | '橙舞舞': '006115', 18 | '暁極': '006116', 19 | '暁将': '006117', 20 | '暁神': '006118', 21 | '暁舞舞': '006119', 22 | '桃極': '006120', 23 | '桃将': '006121', 24 | '桃神': '006122', 25 | '桃舞舞': '006123', 26 | '櫻極': '006124', 27 | '櫻将': '006125', 28 | '櫻神': '006126', 29 | '櫻舞舞': '006127', 30 | '紫極': '006128', 31 | '紫将': '006129', 32 | '紫神': '006130', 33 | '紫舞舞': '006131', 34 | '菫極': '006132', 35 | '菫将': '006133', 36 | '菫神': '006134', 37 | '菫舞舞': '006135', 38 | '白極': '006136', 39 | '白将': '006137', 40 | '白神': '006138', 41 | '白舞舞': '006139', 42 | '雪極': '006140', 43 | '雪将': '006141', 44 | '雪神': '006142', 45 | '雪舞舞': '006143', 46 | '輝極': '006144', 47 | '輝将': '006145', 48 | '輝神': '006146', 49 | '輝舞舞': '006147', 50 | '霸者': '006148', 51 | '舞極': '006149', 52 | '舞将': '006150', 53 | '舞神': '006151', 54 | '舞舞舞': '006152', 55 | '熊極': '055101', 56 | '熊将': '055102', 57 | '熊神': '055103', 58 | '熊舞舞': '055104', 59 | '華極': '109101', 60 | '華将': '109102', 61 | '華神': '109103', 62 | '華舞舞': '109104', 63 | '爽極': '159101', 64 | '爽将': '159102', 65 | '爽神': '159103', 66 | '爽舞舞': '159104', 67 | '煌極': '209101', 68 | '煌将': '209102', 69 | '煌神': '209103', 70 | '煌舞舞': '209104', 71 | '宙極': '259101', 72 | '宙将': '259102', 73 | '宙神': '259103', 74 | '宙舞舞': '259104', 75 | '星極': '309101', 76 | '星将': '309102', 77 | '星神': '309103', 78 | '星舞舞': '309104', 79 | '祭極': '359101', 80 | '祭将': '359102', 81 | '祭神': '359103', 82 | '祭舞舞': '359104', 83 | '祝極': '409101', 84 | '祝将': '409102', 85 | '祝神': '409103', 86 | '祝舞舞': '409104', 87 | '双極': '459101', 88 | '双将': '459102', 89 | '双神': '459103', 90 | '双舞舞': '459104' 91 | } 92 | 93 | VERSION_MAP = { 94 | "真": ["maimai PLUS", "maimai"], 95 | "超": ["maimai GreeN"], 96 | "檄": ["maimai GreeN PLUS"], 97 | "橙": ["maimai ORANGE"], 98 | "暁": ["maimai ORANGE PLUS"], 99 | "晓": ["maimai ORANGE PLUS"], 100 | "桃": ["maimai PiNK"], 101 | "櫻": ["maimai PiNK PLUS"], 102 | "樱": ["maimai PiNK PLUS"], 103 | "紫": ["maimai MURASAKi"], 104 | "菫": ["maimai MURASAKi PLUS"], 105 | "堇": ["maimai MURASAKi PLUS"], 106 | "懂": ["maimai MURASAKi PLUS"], 107 | "白": ["maimai MiLK"], 108 | "雪": ["MiLK PLUS"], 109 | "輝": ["maimai FiNALE"], 110 | "辉": ["maimai FiNALE"], 111 | "熊": ["舞萌DX"], 112 | "華": ["舞萌DX"], 113 | "华": ["舞萌DX"], 114 | "爽": ["舞萌DX 2021"], 115 | "煌": ["舞萌DX 2021"], 116 | "宙": ["舞萌DX 2022"], 117 | "星": ["舞萌DX 2022"], 118 | "祭": ["舞萌DX 2023"], 119 | "祝": ["舞萌DX 2023"], 120 | "双": ["舞萌DX 2024"], 121 | "舞": ["maimai PLUS", "maimai", "maimai GreeN", "maimai GreeN PLUS", "maimai ORANGE", "maimai ORANGE PLUS","maimai PiNK", "maimai PiNK PLUS", "maimai MURASAKi", "maimai MURASAKi PLUS", "maimai MiLK", "MiLK PLUS","maimai FiNALE"], 122 | "霸": ["maimai PLUS", "maimai", "maimai GreeN", "maimai GreeN PLUS", "maimai ORANGE", "maimai ORANGE PLUS","maimai PiNK", "maimai PiNK PLUS", "maimai MURASAKi", "maimai MURASAKi PLUS", "maimai MiLK", "MiLK PLUS","maimai FiNALE"] 123 | } 124 | 125 | TRADITIONAL2SIMPLIFIED = { 126 | "真": "真", 127 | "超": "超", 128 | "檄": "檄", 129 | "橙": "橙", 130 | "暁": "暁", 131 | "晓": "暁", 132 | "桃": "桃", 133 | "櫻": "櫻", 134 | "樱": "櫻", 135 | "紫": "紫", 136 | "菫": "菫", 137 | "堇": "菫", 138 | "懂": "菫", 139 | "白": "白", 140 | "雪": "雪", 141 | "輝": "輝", 142 | "辉": "輝", 143 | "熊": "熊", 144 | "華": "華", 145 | "华": "華", 146 | "爽": "爽", 147 | "煌": "煌", 148 | "宙": "宙", 149 | "星": "星", 150 | "祭": "祭", 151 | "祝": "祝", 152 | "双": "双", 153 | "舞": "舞", 154 | "霸": "霸", 155 | "极": "極" 156 | } 157 | 158 | VERSION_DF_MAP = { 159 | "真":["maimai PLUS","maimai"], 160 | "超":["maimai GreeN"], 161 | "檄":["maimai GreeN PLUS"], 162 | "橙":["maimai ORANGE"], 163 | "晓":["maimai ORANGE PLUS"], 164 | "桃":["maimai PiNK"], 165 | "樱":["maimai PiNK PLUS"], 166 | "紫":["maimai MURASAKi"], 167 | "堇":["maimai MURASAKi PLUS"], 168 | "白":["maimai MiLK"], 169 | "雪":["MiLK PLUS"], 170 | "辉":["maimai FiNALE"], 171 | "熊":["maimai でらっくす"], 172 | "华":["maimai でらっくす"], 173 | "爽":["maimai でらっくす Splash"], 174 | "煌":["maimai でらっくす Splash"], 175 | "宙":["maimai でらっくす UNiVERSE"], 176 | "星":["maimai でらっくす UNiVERSE"], 177 | "祭":["maimai でらっくす FESTiVAL"], 178 | "祝":["maimai でらっくす FESTiVAL"], 179 | "双":["maimai でらっくす BUDDiES"], 180 | "舞": ["maimai PLUS", "maimai", "maimai GreeN", "maimai GreeN PLUS", "maimai ORANGE", "maimai ORANGE PLUS","maimai PiNK", "maimai PiNK PLUS", "maimai MURASAKi", "maimai MURASAKi PLUS", "maimai MiLK", "MiLK PLUS","maimai FiNALE"], 181 | "霸": ["maimai PLUS", "maimai", "maimai GreeN", "maimai GreeN PLUS", "maimai ORANGE", "maimai ORANGE PLUS","maimai PiNK", "maimai PiNK PLUS", "maimai MURASAKi", "maimai MURASAKi PLUS", "maimai MiLK", "MiLK PLUS","maimai FiNALE"] 182 | } 183 | 184 | VERSION_LOGO_MAP = { 185 | "maimai":"100", 186 | "maimai PLUS":"110", 187 | "maimai GreeN":"120", 188 | "maimai GreeN PLUS":"130", 189 | "maimai ORANGE":"140", 190 | "maimai ORANGE PLUS":"150", 191 | "maimai PiNK":"160", 192 | "maimai PiNK PLUS":"170", 193 | "maimai MURASAKi":"180", 194 | "maimai MURASAKi PLUS":"185", 195 | "maimai MiLK":"190", 196 | "MiLK PLUS":"195", 197 | "maimai FiNALE":"199", 198 | "舞萌DX":"200", 199 | "舞萌DX 2021":"214", 200 | "舞萌DX 2022":"220", 201 | "舞萌DX 2023":"230", 202 | "舞萌DX 2024":"240" 203 | } 204 | 205 | VERSION_EZ_MAP = { 206 | "PLUS":"真", 207 | "maimai": "真", 208 | "GreeN":"超", 209 | "GreeN PLUS":"檄", 210 | "ORANGE":"橙", 211 | "ORANGE PLUS":"暁", 212 | "PiNK":"桃", 213 | "PiNK PLUS":"櫻", 214 | "MURASAKi":"紫", 215 | "MURASAKi PLUS":"菫", 216 | "MiLK":"白", 217 | "MiLK PLUS":"雪", 218 | "FiNALE":"輝", 219 | "FiNALE":"辉", 220 | "舞萌DX":"熊,華", 221 | "舞萌DX 2021":"爽、煌", 222 | "舞萌DX 2022":"宙、星", 223 | "舞萌DX 2023":"祭、祝", 224 | "舞萌DX 2024":"双" 225 | } 226 | 227 | ALL_VERSION_LIST = ["maimai PLUS","maimai","maimai GreeN","maimai GreeN PLUS","maimai ORANGE","maimai ORANGE PLUS","maimai PiNK","maimai PiNK PLUS","maimai MURASAKi","maimai MURASAKi PLUS","maimai MiLK","MiLK PLUS","maimai FiNALE","maimai でらっくす","maimai でらっくす FESTiVAL","maimai でらっくす UNiVERSE","maimai でらっくす Splash","maimai でらっくす BUDDiES"] 228 | 229 | # 删除复活过滤曲 230 | DELETED_MUSIC = [70,341,451,460,455,853,792,146,10146,189,419,687,731,11267,688,11213,712,11253,510,185,190,524,11347,11396,11407,11408,11635] 231 | 232 | # 当前游戏版本 233 | NOW_VERSION = "舞萌DX 2024" 234 | 235 | # 水鱼开发令牌 236 | DEVELOPER_TOKEN = "" 237 | 238 | 239 | USER_POKE_MESSAGE = [ 240 | '我觉得吃披萨应该加95号汽油,并且应该加到试管底部1/3的位置,这样是为了拉格朗日点能够在爱因斯坦质能方程中找到属于自己的北回归线,如果不小心加错加了92号汽油,那就不好办了,首先要找到老爹让他念出戊戌变法,防止发动机和神经中枢对撞产生等离子火花和伽马射线,按照孔子的说法,我们还要把进化论里的化学方程式配平,这样才能让海尔兄弟放过舒克和贝塔', 241 | '太阳的海平面上每天都会升起月亮,从而引起海绵宝宝生物钟的混乱导致派大星流产,所以必须要张果老带着赛亚人去日本才能阻止美国入侵伊拉克', 242 | '我觉得太阳每天早上会从阿尔卑斯山脉的鲨鱼口中降落,哈姆雷特携带着超级赛亚人回到了花果山,而且如来的手指捏到了观音的八戒,所以唐僧带着三毛去珍珠港进行了广岛核爆演习', 243 | '我认为好的意大利粥的做法应该是五常大米加一点长白山上的特仑苏搅拌成薄薄的厚片,因为这样可以生成美拉德反应,并且有利于经济的飞速发展,从而得出永乐大典失传的真相,爱因斯坦福说过,好的阿尔法突袭可以在一瞬间斩杀程咬金,防止他开大,再向B区扔个闪光蛋,这样才能在明朝小冰河时期拯救更多的黑猫警长。', 244 | '我认为在南北回归直线上不应该存在钢筋混凝土的哥德巴赫猜想微积分证明,因为海绵宝宝的生理期在夏季大洋环流会收到偏振效应的阻碍,从而偏离黑子熵增的观测,量子力学在其中发挥的作用就会受到阿尔法突袭的元婴期影响,所以大骨面提鲜运用的的收割理论在一定程度上不是那么具有波粒二象性', 245 | '我也不干沟通。我个人认为老谭酸菜牛肉面就应该配肾结石,因为双子座的第六感很容易召唤出塞牙超人。金字塔的拐点和东非大裂谷的粘度通过微积分可以证明猪腰子为什么嚼不烂。而且,商鞅当初变法就是为了满足欧洲贵族的生理需求,以便在百年后顺利让老佛爷生下二次元萝莉,这其实也不会影响西游记和葫芦娃在哔哩哔哩上的联动。', 246 | '我不敢苟同这样的说法,首先我认为老坛酸菜应该拥有准考证号,这样更能证明水是剧毒,但李白和白垩纪大灭绝的关系是属于乘法口诀表里的广义相对论,因此我推算出牛肉和原子弹的味道一样,喜欢吃肉和1+1=2这两种观点有很大的冲突,有人会觉得这观点偏激了,我们可以得知牛顿被苹果砸中后发现了钢筋混凝土喜欢鲨鱼后让莫比乌斯环分成两面,我们也可以得出西伯利亚与三体视频聊天通话中谈到的是米老鼠是应该按F,还是加鱼子酱的话题,总体来说,这都不影响潘子与虎哥在一起商讨鲨鱼应该吃混凝土还是穿内裤的问题', 247 | '我个人认为,这个意大利面就应该拌42号混凝土,因为这个螺丝钉的长度,它很容易会直接影响到挖掘机的扭矩你知道吧,你往里砸的时候,一瞬间它就会产生大量的高能蛋白,俗称ufo,会严重影响经济的发展,甚至对整个太平洋以及充电器都会造成一定的核污染,你知道吧啊?再者说,根据这个勾股定理,你可以很容易地推断出人工饲养的东条英机它是可以捕获野生的三角函数的,所以说这个这个这个这个,你不管秦始皇的切面是否具有放射性啊,特朗普的N次方是否含有沉淀物,都不影响这个沃尔玛跟维尔康在南极会合。', 248 | '其实我觉得吃盘子就得用面包端,因为全世界大约有3000种蚊子,每当蚊子扇动翅膀的时候,会产生温室效应,从而导致全世界引发大地震,地球偏离轨道,众所周知,世界万物都是有引力的,所以导致了成千上万人都经历了曼德拉效应,从而认为吃面包得用盘子端', 249 | '不敢苟同,我认为炒意大利面应该拌老干妈,因为螺丝钉向内扭的时候会产生二氧化碳,不利于经济发展,并且野生的乌鸦也会捕食三角函数。所以不论承重墙能不能打赢DK,宋江也能赢得世界杯', 250 | 'xray有孩子这件事,我觉得和勾股定理是有关系的,热带雨林的蝴蝶煽动了翅膀导致了大气逆辐射的增加,这使得春天来临的速度加快,春天来的快就会让鸡交配更快,更快就意味着未来更多的鸡肉,这能使得肯德基持续采取疯狂星期X的计划,这个时候,由于厄尔尼诺的效应,产生了伽马射线,这样我们才能让xray生出更多的疯狂星期四,得出结论:V我50' 251 | ] 252 | 253 | FORTUNE_LIST = [ 254 | ['水群', '获得耶耶指点!飞升了', '没有人理你!'], 255 | ['出勤', '我也不知道你为什么宜查分,因为主人是这么写的~', '因为我会骂你啊哈哈哈哈哈'], 256 | ['收歌', 'Ap+1+1+1+1了呢~', '我又性了😅('], 257 | ['肯德基', '妈你戳,有什么好戳的', '你会被我骂~'], 258 | ['睡觉', '多睡觉身体好~', '一天就这样过去了,都不看Xray一眼('], 259 | ['练底力', '说不定底力今天就蹭蹭蹭的涨喽~', '越打越菜.......'], 260 | ['打旧框', '手感异常的好哇!!!', '机厅没网😅('], 261 | ['学习', '活到老学到老(', '怎么可能有这种事,w论如何都要学习的好吧~'], 262 | ['干饭', '吃饱了好推分捏~', '吃饱了就想摆烂了惹......'], 263 | ['抓绝赞', '一抓一个准!', '一落二爆三Miss~'], 264 | ['下埋', '路人小妹妹觉得你好菜的...', '润!!!!!!'] 265 | ] 266 | 267 | # QQ号权限 268 | # 机修() 269 | BOT_STAFF = [381268035] 270 | # BOT管理员(更新完成表) 271 | BOT_DATA_ADMINISTRATOR = [381268035] 272 | # BOT数据管理工作人员(开放黑白名单、直接添加别名) 273 | BOT_DATA_STAFF = [381268035] 274 | 275 | 276 | # 群号权限 277 | # 保存图片群权限(添加龙图、抽象画) 278 | SAVE_IMAGE = [1019756397] 279 | 280 | 281 | # Mongo服务器配置 282 | MONGO_USERNAME = '' 283 | MONGO_PASSWORD = '' 284 | MONGO_HOST = '127.0.0.1' 285 | MONGO_PORT = 21017 286 | # 身份校验数据库 287 | MONGO_DATABASE = 'admin' 288 | 289 | # 连接数据库 290 | MONGO_DB = 'xray-mai-bot' 291 | 292 | # 高德APIKEY(附近mai) 293 | API_KEY = "" 294 | -------------------------------------------------------------------------------- /src/libraries/GLOBAL_PATH.py: -------------------------------------------------------------------------------- 1 | CHN_MUSIC_DATA_URL = 'https://download.fanyu.site/maimai/music_data.json' 2 | ABSTRACT_DOWNLOAD_URL = 'https://download.fanyu.site/abstract/' 3 | # /路径 4 | RUNTIME_LOG_PATH = "/data/log" 5 | CUSTOM_LOG_PATH = "/data/clog" 6 | 7 | # DATA路径 8 | 9 | DATA_MAIMAI_PATH = "data/maimai" 10 | DATA_ADMIN_PATH = "data/admin" 11 | DATA_XRAY_MAI_BOT_DB_PATH = "data/xray-mai-bot-db" 12 | DATA_TEMP_PATH = "data/tempdata" 13 | 14 | 15 | # STATIC路径 16 | 17 | DRAGON_PATH = "src/static/dragon" 18 | HELP_PATH = "src/static/help.png" 19 | REWARD_QR_PATH = "src/static/打钱.png" # 打赏码 20 | MAIDX_LOCATION_PATH = "src/static/location.json" # 新开业Mai 21 | 22 | # 收藏品 23 | FRAME_PATH = "src/static/maimaidx/game_static_frame" 24 | PLATE_PATH = "src/static/maimaidx/game_static_plate" 25 | CUSTOM_PLATE_PATH = "src/static/maimaidx/game_static_plate/custom" 26 | ICON_PATH = 'src/static/maimaidx/game_static_icon' 27 | 28 | # 封面 29 | ABSTRACT_COVER_PATH = "src/static/maimaidx/abstract_cover" 30 | NORMAL_COVER_PATH = "src/static/maimaidx/normal_cover" 31 | CHUNITHN_COVER_PATH = "src/static/chunithm/cover" 32 | 33 | # 字体 34 | FONT_PATH = "src/static/fonts" 35 | 36 | # 完成表 37 | COMPLETION_STATUS_TABLE_PATH = "src/static/maimaidx/completion_status_table" 38 | NEW_COMPLETION_STATUS_TABLE_PATH = "src/static/maimaidx/completion_status_table/plate_completion_status_table" 39 | 40 | # 歌曲详细信息 41 | MAIMAI_MUSIC_DATA = "src/static/maimaidx/maimai_music_data" 42 | UTAGE_MAIMAI_MUSIC_DATA = "src/static/maimaidx/utage_song_data" 43 | FORTUNE_PATH = "src/static/maimaidx/fortune" 44 | 45 | 46 | # B50 47 | NORMAL_BEST_50_PATH = "src/static/maimaidx/player_best_50_normal" 48 | ABSTRACT_BEST_50_PATH = "src/static/maimaidx/player_best_50_abstract" 49 | OLD_BEST_50_PATH = "src/static/maimaidx/player_best_50_old_design" 50 | 51 | # INFO 52 | PLAYER_MUSIC_SCORE_PATH = "src/static/maimaidx/player_music_score" 53 | 54 | RANK_TABLE_PATH = "src/static/maimaidx/rank_table" 55 | 56 | 57 | MAIMAIDX_PATH = "src/static/maimaidx" 58 | 59 | COLLECTIBLES_PATH = "src/static/maimaidx/collectibles" 60 | 61 | # CHU 62 | CHUNITHN_DATA_PATH = "src/static/chunithm" 63 | CHUNITHN_MUSIC_DATA_PATH = "src/static/chunithm/music_data" -------------------------------------------------------------------------------- /src/libraries/GLOBAL_RULE.py: -------------------------------------------------------------------------------- 1 | 2 | from src.libraries.GLOBAL_CONSTANT import BOT_DATA_STAFF,BOT_DATA_ADMINISTRATOR 3 | from nonebot.adapters.onebot.v11 import GroupMessageEvent 4 | from nonebot.rule import Rule 5 | 6 | def check_is_bot_data_staff(): 7 | async def _checker(event: GroupMessageEvent) -> bool: 8 | if event.user_id in BOT_DATA_STAFF: 9 | return True 10 | else: 11 | return False 12 | return Rule(_checker) 13 | 14 | async def check_is_bot_admin(event: GroupMessageEvent) -> bool: 15 | return event.user_id in BOT_DATA_ADMINISTRATOR -------------------------------------------------------------------------------- /src/libraries/data_handle/abstract_db_handle.py: -------------------------------------------------------------------------------- 1 | import random 2 | import pymongo 3 | from src.libraries.GLOBAL_CONSTANT import MONGO_HOST,MONGO_DATABASE,MONGO_PASSWORD,MONGO_PORT,MONGO_USERNAME,MONGO_DB 4 | class Abstract(object): 5 | def __init__(self): 6 | username = MONGO_USERNAME 7 | password = MONGO_PASSWORD 8 | host = MONGO_HOST 9 | port = MONGO_PORT 10 | database_name = MONGO_DATABASE 11 | connection_string = f'mongodb://{username}:{password}@{host}:{port}/{database_name}' 12 | self.client = pymongo.MongoClient(connection_string) 13 | self.db = self.client[MONGO_DB] 14 | self.abstract_collection = self.db['abstract'] 15 | self.counters_collection = self.db['counters'] 16 | 17 | def get_abstract_id_list(self): 18 | search_data = self.abstract_collection.find() 19 | music_list = [int(i['music_id']) for i in search_data] 20 | return music_list 21 | 22 | def get_abstract_file_name(self, music_id): 23 | music_id = str(music_id) 24 | if int(music_id) in self.get_abstract_id_list(): 25 | music_abstract_data = self.abstract_collection.find_one( 26 | {"music_id": music_id}) 27 | if music_abstract_data: 28 | rdr = random.choice(music_abstract_data['abstract_data']) 29 | return rdr["file_name"], rdr["nickname"] 30 | else: 31 | return f"{str(music_id)}_1", "Xray Art Team" 32 | else: 33 | return f"{str(music_id)}", "抽象画未收录" 34 | 35 | 36 | def get_abstract_data_by_id(self, music_id: str): 37 | doc = self.abstract_collection.find_one({"music_id": music_id}) 38 | if doc: 39 | abstract_data = doc['abstract_data'] 40 | if abstract_data: 41 | return abstract_data 42 | else: 43 | return [{ 44 | "user_id": 1919810, 45 | "nickname": "Xray Art Team", 46 | "file_name": f"{str(music_id)}_1" 47 | }] 48 | else: 49 | return [] 50 | 51 | def get_abstract_data(self): 52 | ams = self.get_abstract_id_list() 53 | abstract_data = list(self.abstract_collection.find()) 54 | ad = {"abstract": abstract_data, "ams": ams} 55 | return ad 56 | 57 | def get_abstract_file_name_all(self): 58 | music_abstract_data = self.abstract_collection.find({}) 59 | 60 | file_name_map = {} 61 | for item in music_abstract_data: 62 | music_id = item['music_id'] 63 | rdr = random.choice(item['abstract_data']) 64 | file_name_map[music_id] = rdr["file_name"] 65 | 66 | return file_name_map 67 | abstract = Abstract() 68 | -------------------------------------------------------------------------------- /src/libraries/data_handle/alias_db_handle.py: -------------------------------------------------------------------------------- 1 | import pymongo 2 | import time 3 | from datetime import datetime,timedelta 4 | from src.libraries.maimai.maimaidx_music import total_list 5 | import re 6 | from src.libraries.GLOBAL_CONSTANT import MONGO_HOST,MONGO_DATABASE,MONGO_PASSWORD,MONGO_PORT,MONGO_USERNAME,MONGO_DB 7 | 8 | class Alias(object): 9 | def __init__(self): 10 | username = MONGO_USERNAME 11 | password = MONGO_PASSWORD 12 | host = MONGO_HOST 13 | port = MONGO_PORT 14 | database_name = MONGO_DATABASE 15 | connection_string = f'mongodb://{username}:{password}@{host}:{port}/{database_name}' 16 | self.client = pymongo.MongoClient(connection_string) 17 | self.db = self.client[MONGO_DB] 18 | self.alias_collection = self.db['alias'] 19 | self.alias_examine_collection = self.db['alias_examine'] 20 | self.counters_collection = self.db['counters'] 21 | 22 | def queryMusicByAlias(self,alias: str): 23 | if list(set(list(alias))) == ['.']: 24 | return [] 25 | escaped_alias = re.escape(alias) 26 | search_result = list(self.alias_collection.find( 27 | {"alias":{'$regex': f'^{escaped_alias}$', '$options': 'i'},"enable":True}, 28 | {"_id":0,"music_id":1})) 29 | music_ids = [m['music_id'] for m in search_result] 30 | return music_ids 31 | 32 | def SearchAlias(self,music_id:str): 33 | alias_data = self.alias_collection.find_one({"music_id":music_id}) 34 | return list(set(alias_data['alias'])) 35 | 36 | 37 | alias = Alias() 38 | -------------------------------------------------------------------------------- /src/libraries/data_handle/black_list_handle.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | from typing import Dict 4 | from nonebot.log import logger 5 | from src.libraries.GLOBAL_PATH import DATA_ADMIN_PATH 6 | class Admin(object): 7 | def __init__(self): 8 | self.data_dir = Path(DATA_ADMIN_PATH).absolute() 9 | self.admin_path = self.data_dir / "admin.json" 10 | self.data_dir.mkdir(parents=True, exist_ok=True) 11 | self.data: Dict[str, Dict[str, Dict[str, list]]] = {} 12 | self.__load() 13 | 14 | def __load(self): 15 | if self.admin_path.exists() and self.admin_path.is_file(): 16 | with self.admin_path.open("r", encoding="utf-8") as f: 17 | data: dict = json.load(f) 18 | self.data = data 19 | logger.success("读取词库位于 " + str(self.admin_path)) 20 | else: 21 | self.data = {"group_id":[],"user_id":[]} 22 | self.__save() 23 | logger.success("创建词库位于 " + str(self.admin_path)) 24 | 25 | def __save(self): 26 | with self.admin_path.open("w", encoding="utf-8") as f: 27 | json.dump(self.data, f, ensure_ascii=False, indent=4) 28 | 29 | def get_groupid(self): 30 | return self.data.get('group_id',[]) 31 | 32 | def get_userid(self): 33 | return self.data.get('user_id',[]) 34 | 35 | def get_along_black(self): 36 | return self.data.get('along_black',[]) 37 | 38 | def add_group(self,group_id:int): 39 | if group_id in self.data['group_id']: 40 | return f'{str(group_id)}已开启,请勿重复开启' 41 | else: 42 | self.data['group_id'].append(group_id) 43 | self.__save() 44 | return f'{str(group_id)}开启成功' 45 | 46 | def del_group(self,group_id:int): 47 | if group_id in self.data['group_id']: 48 | self.data['group_id'].remove(group_id) 49 | self.__save() 50 | return f'{str(group_id)}关闭成功' 51 | else: 52 | return f'{str(group_id)}未开启,无法关闭群' 53 | 54 | def add_user(self,user_id:int): 55 | if user_id in self.data['user_id']: 56 | return f'{str(user_id)}已拉黑,请勿重复拉黑' 57 | else: 58 | self.data['user_id'].append(user_id) 59 | self.__save() 60 | return f'{str(user_id)}拉黑成功' 61 | 62 | def del_user(self,user_id:int): 63 | if user_id in self.data['user_id']: 64 | self.data['user_id'].remove(user_id) 65 | self.__save() 66 | return f'{str(user_id)}解除黑名单成功' 67 | else: 68 | return f'{str(user_id)}未拉黑,无法解除黑名单' 69 | 70 | def add_along_black(self,group_id:int): 71 | if group_id in self.data['along_black']: 72 | return f'{str(group_id)}已关闭龙图模式,请勿重复关闭' 73 | else: 74 | self.data['along_black'].append(group_id) 75 | self.__save() 76 | return f'{str(group_id)}关闭龙图模式成功' 77 | 78 | def del_along_black(self,group_id:int): 79 | if group_id in self.data['along_black']: 80 | self.data['along_black'].remove(group_id) 81 | self.__save() 82 | return f'{str(group_id)}恢复龙图模式成功' 83 | else: 84 | return f'{str(group_id)}未关闭,无法开启龙图模式' 85 | admin = Admin() 86 | -------------------------------------------------------------------------------- /src/libraries/data_handle/userdata_handle.py: -------------------------------------------------------------------------------- 1 | import pymongo 2 | from src.libraries.GLOBAL_CONSTANT import MONGO_HOST,MONGO_DATABASE,MONGO_PASSWORD,MONGO_PORT,MONGO_USERNAME,MONGO_DB 3 | 4 | 5 | class UserData(object): 6 | def __init__(self): 7 | username = MONGO_USERNAME 8 | password = MONGO_PASSWORD 9 | host = MONGO_HOST 10 | port = MONGO_PORT 11 | database_name = MONGO_DATABASE 12 | connection_string = f'mongodb://{username}:{password}@{host}:{port}/{database_name}' 13 | self.client = pymongo.MongoClient(connection_string) 14 | self.db = self.client[MONGO_DB] 15 | self.userdata_collection = self.db['xray_user_data'] 16 | 17 | def getUserData(self,user_id:str): 18 | user_data = self.userdata_collection.find_one({"_id":user_id}) 19 | if user_data: 20 | print(user_data) 21 | return user_data 22 | else: 23 | return {} 24 | 25 | # def setUserConfig(self,user_id:str,group_id:int,arg_name:str,value): 26 | # try: 27 | # data = {arg_name: value} 28 | # existing_data = self.userdata_collection.find_one({'_id': user_id}) 29 | # if existing_data: 30 | # data['create_group'] = existing_data.get("group",group_id) 31 | # self.userdata_collection.update_one({'_id': user_id}, {'$set': data}) 32 | # else: 33 | # data['_id'] = user_id 34 | # data['create_group'] = group_id 35 | # self.userdata_collection.insert_one(data) 36 | # return True 37 | # except: 38 | # return False 39 | 40 | userdata = UserData() 41 | -------------------------------------------------------------------------------- /src/libraries/execution_time.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | def timing_decorator(func): 4 | def wrapper(*args, **kwargs): 5 | start_time = time.time() 6 | result = func(*args, **kwargs) 7 | end_time = time.time() 8 | execution_time = end_time - start_time 9 | print(f"方法 {func.__name__} 的执行时间: {execution_time:.6f} 秒") 10 | return result 11 | return wrapper 12 | 13 | def timing_decorator_async(func): 14 | async def wrapper(*args, **kwargs): 15 | start_time = time.time() 16 | result = await func(*args, **kwargs) 17 | end_time = time.time() 18 | execution_time = end_time - start_time 19 | print(f"异步方法 {func.__name__} 的执行时间: {execution_time:.6f} 秒") 20 | return result 21 | return wrapper 22 | -------------------------------------------------------------------------------- /src/libraries/image_handle/image.py: -------------------------------------------------------------------------------- 1 | import base64 2 | from io import BytesIO 3 | from PIL import ImageFont, ImageDraw, Image 4 | from src.libraries.GLOBAL_PATH import FONT_PATH 5 | 6 | boy_friend_font_path = f"{FONT_PATH}/boy_friend.ttf" 7 | 8 | def draw_text(img_pil, text, offset_x): 9 | draw = ImageDraw.Draw(img_pil) 10 | font = ImageFont.truetype(boy_friend_font_path, 48) 11 | width, height = draw.textsize(text, font) 12 | x = 5 13 | if width > 390: 14 | font = ImageFont.truetype(boy_friend_font_path, int(390 * 48 / width)) 15 | width, height = draw.textsize(text, font) 16 | else: 17 | x = int((400 - width) / 2) 18 | draw.rectangle((x + offset_x - 2, 360, x + 2 + width + offset_x, 360 + height * 1.2), fill=(0, 0, 0, 255)) 19 | draw.text((x + offset_x, 360), text, font=font, fill=(255, 255, 255, 255)) 20 | 21 | 22 | def text_to_image(text): 23 | font = ImageFont.truetype(boy_friend_font_path, 30) 24 | padding = 10 25 | margin = 4 26 | text_list = text.split('\n') 27 | max_width = 0 28 | for text in text_list: 29 | w, h = font.getsize(text) 30 | max_width = max(max_width, w) 31 | wa = max_width + padding * 2 32 | ha = h * len(text_list) + margin * (len(text_list) - 1) + padding * 2 33 | i = Image.new('RGB', (wa, ha), color=(255, 255, 255)) 34 | draw = ImageDraw.Draw(i) 35 | for j in range(len(text_list)): 36 | text = text_list[j] 37 | draw.text((padding, padding + j * (margin + h)), text, font=font, fill=(0, 0, 0)) 38 | return i 39 | 40 | def image_to_base64(img, format='PNG'): 41 | output_buffer = BytesIO() 42 | img.save(output_buffer, format) 43 | byte_data = output_buffer.getvalue() 44 | base64_str = base64.b64encode(byte_data) 45 | return base64_str 46 | 47 | # 辅助函数:分割文本以适应最大宽度 48 | def split_text_to_lines(text, max_width, font): 49 | lines = [] 50 | current_line = "" 51 | for char in text: 52 | # 检查当前行的宽度加上新字符的宽度是否超过最大宽度 53 | text_width, text_height = font.getsize(current_line + char) 54 | if text_width <= max_width: 55 | current_line += char 56 | else: 57 | # 如果超过最大宽度,则将当前行添加到列表中,并开始新行 58 | lines.append(current_line) 59 | current_line = char 60 | # 添加最后一行(如果有的话) 61 | if current_line: 62 | lines.append(current_line) 63 | return "\n".join(lines) -------------------------------------------------------------------------------- /src/libraries/maimai/completion_status_table/new_pcst_base_img.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from PIL import Image, ImageDraw,ImageFont 3 | from src.libraries.maimai.completion_status_table.utils import get_abstract_cover_path,get_nomal_cover_path 4 | from src.libraries.maimai.maimaidx_music import total_list 5 | from src.libraries.data_handle.abstract_db_handle import abstract 6 | from src.libraries.GLOBAL_CONSTANT import VERSION_MAP,DELETED_MUSIC,VERSION_DF_MAP,MAIPLATE_FILE_ID_MAP,TRADITIONAL2SIMPLIFIED 7 | from src.libraries.GLOBAL_PATH import FONT_PATH,NEW_COMPLETION_STATUS_TABLE_PATH,COMPLETION_STATUS_TABLE_PATH,PLATE_PATH,ICON_PATH 8 | from src.libraries.data_handle.userdata_handle import userdata 9 | from pathlib import Path 10 | import requests 11 | def calculate(num,i): 12 | if num % i == 0: 13 | return num // i 14 | else: 15 | return num // i + 1 16 | 17 | def get_music_info(finishs,id:int,level:int): 18 | result = finishs['verlist'] 19 | for item in result: 20 | if item['id'] == id and item['level_index'] == level: 21 | return {'fc':item['fc'],'fs':item['fs'],'achievements':item['achievements']} 22 | return {} 23 | 24 | 25 | def query_user_plate_data(versions,user_id:str): 26 | version_list = total_list.by_version_for_plate(versions) 27 | payload = {'qq':user_id,'version':versions} 28 | r = requests.post("https://www.diving-fish.com/api/maimaidxprober/query/plate", json=payload) 29 | finishs = r.json() 30 | version_song_info = {} 31 | for song in version_list: 32 | version_song_info[song.id] = {} 33 | for index ,level in enumerate(song['level']): 34 | version_song_info[song.id][index] = get_music_info(finishs,int(song.id),index) 35 | return version_song_info 36 | 37 | def generate_music_data(version:str,is_abstract:bool): 38 | result_list = total_list.by_versions_for_cn(VERSION_MAP[version]) 39 | default_song_list = {"15":[],"14+":[],"14":[],"13+":[],"13":[],"12+":[],"12":[],"11+":[],"11":[],"10+":[],"10":[],"9+":[],"9":[],"8+":[],"8":[],"7+":[],"7":[],"6":[],"5":[],"4":[],"3":[],"2":[],"1":[]} 40 | 41 | for song_info in result_list: 42 | if int(song_info.id) in DELETED_MUSIC: 43 | continue 44 | if int(song_info.id) > 99999: 45 | continue 46 | level= song_info.level[3] 47 | default_song_list[level].append(song_info.id) 48 | if version in ['舞霸']: 49 | if len(song_info.level) == 5: 50 | level= song_info.level[4] 51 | default_song_list[level].append(song_info.id) 52 | music_data_hight = 42 53 | for music_list in default_song_list.values(): 54 | 55 | item = len(music_list) 56 | if item > 0: 57 | music_data_hight = music_data_hight+38 58 | music_data_hight = music_data_hight+(127*calculate(item,10)) 59 | 60 | music_data_hight = music_data_hight-38 61 | music_data_img = Image.new("RGBA",(1420,music_data_hight),(255,255,255,255)) 62 | 63 | ix = 216 64 | iy = 42 65 | 66 | if is_abstract: 67 | abstract_cover_file_map = abstract.get_abstract_file_name_all() 68 | 69 | 70 | for item in default_song_list.items(): 71 | if item[1]: 72 | count = 0 73 | rcount = 0 74 | 75 | level_log_img = Image.open(f"{NEW_COMPLETION_STATUS_TABLE_PATH}/level/{item[0]}.png").convert('RGBA') 76 | music_data_img.paste(level_log_img,(40,iy),level_log_img) 77 | for song in range(0,len(default_song_list[item[0]])): 78 | id = default_song_list[item[0]][song] 79 | 80 | music_box = Image.new("RGBA",(113,125),(255,255,255,0)) 81 | 82 | if is_abstract: 83 | cover_path = get_abstract_cover_path(int(id),abstract_cover_file_map) 84 | else: 85 | cover_path = get_nomal_cover_path(int(id)) 86 | cover = Image.open(cover_path).convert('RGBA') 87 | cover = cover.resize((95, 95), Image.ANTIALIAS) 88 | music_box.paste(cover, (9, 8), cover) 89 | 90 | music_bg = Image.open(f"{NEW_COMPLETION_STATUS_TABLE_PATH}/song/框.png").convert('RGBA') 91 | music_box.paste(music_bg, (0, 0), music_bg) 92 | musicImageDraw = ImageDraw.Draw(music_box) 93 | tempFont = ImageFont.truetype(FONT_PATH + "/GlowSansSC-Normal-Bold.otf", 10, encoding='utf-8') 94 | fx,fy = tempFont.getsize(str(id)) 95 | musicImageDraw.text((92 - (fx/2), 113 - (fy/2)), str(id), font=tempFont, fill="white") 96 | music_data_img.paste(music_box,(ix,iy),music_box) 97 | 98 | 99 | count = count + 1 100 | rcount = rcount + 1 101 | ix = ix + 113 102 | if count == 10 and len(default_song_list[item[0]])!= rcount: 103 | ix = 216 104 | iy = iy+127 105 | count = 0 106 | iy = iy + 127+38 107 | ix = 216 108 | return music_data_img 109 | 110 | def generate_full_version(version:str,is_abstract:bool): 111 | music_data_img = generate_music_data(version,is_abstract) 112 | music_data_img_hight = music_data_img.height 113 | full_version_hight = 107+700+music_data_img_hight+63+141+57+73 114 | full_version_img = Image.new("RGBA",(1600,full_version_hight),(255,255,255,0)) 115 | if Path(f"{NEW_COMPLETION_STATUS_TABLE_PATH}/background/有模糊/{version}.png").exists(): 116 | bg_img = Image.open(f"{NEW_COMPLETION_STATUS_TABLE_PATH}/background/有模糊/{version}.png").convert('RGBA') 117 | full_version_img.paste(bg_img,(0,0),bg_img) 118 | else: 119 | return None 120 | top_img = Image.open(f"{NEW_COMPLETION_STATUS_TABLE_PATH}/top.png").convert('RGBA') 121 | full_version_img.paste(top_img,(90,107),top_img) 122 | full_version_img.paste(music_data_img,(90,107+700),music_data_img.split()[3]) 123 | foot_img = Image.open(f"{NEW_COMPLETION_STATUS_TABLE_PATH}/foot.png").convert('RGBA') 124 | full_version_img.paste(foot_img,(90,107+700+music_data_img_hight),foot_img.split()[3]) 125 | credits_img = Image.open(f"{NEW_COMPLETION_STATUS_TABLE_PATH}/Credits.png").convert('RGBA') 126 | full_version_img.paste(credits_img,(0,107+700+music_data_img_hight+63+57),credits_img.split()[3]) 127 | 128 | text_bg = Image.open(f"{NEW_COMPLETION_STATUS_TABLE_PATH}/text/background.png").convert('RGBA') 129 | full_version_img.paste(text_bg,(143,402),text_bg.split()[3]) 130 | 131 | progress_bg = Image.open(f"{NEW_COMPLETION_STATUS_TABLE_PATH}/progress/background.png").convert('RGBA') 132 | full_version_img.paste(progress_bg,(589,402),progress_bg.split()[3]) 133 | if is_abstract: 134 | full_version_img.save(f"{COMPLETION_STATUS_TABLE_PATH}/new_pcst_base_img_abstract/{version}.png") 135 | else: 136 | full_version_img.save(f"{COMPLETION_STATUS_TABLE_PATH}/new_pcst_base_img_normal/{version}.png") 137 | return full_version_img 138 | 139 | def check_completion_conditions(plate_mode,music_data): 140 | if not music_data: 141 | return False 142 | if plate_mode == '将': 143 | return music_data['achievements'] >= 100 144 | 145 | elif plate_mode == '极': 146 | return music_data['fc'] in ['fc','ap','fcp','app'] 147 | 148 | elif plate_mode == '神': 149 | return music_data['fc'] in ['ap','app'] 150 | 151 | elif plate_mode == '舞舞': 152 | return music_data['fs'] in ['fsd','fsdp'] 153 | 154 | def generate_user_music_data(version:str,plate_mode,uid:str): 155 | result_list = total_list.by_versions_for_cn(VERSION_MAP[version]) 156 | version_song_info = query_user_plate_data(VERSION_DF_MAP[version],uid) 157 | default_song_list = {"15":[],"14+":[],"14":[],"13+":[],"13":[],"12+":[],"12":[],"11+":[],"11":[],"10+":[],"10":[],"9+":[],"9":[],"8+":[],"8":[],"7+":[],"7":[],"6":[],"5":[],"4":[],"3":[],"2":[],"1":[]} 158 | 159 | for song_info in result_list: 160 | if int(song_info.id) in DELETED_MUSIC: 161 | continue 162 | if int(song_info.id) > 99999: 163 | continue 164 | level= song_info.level[3] 165 | default_song_list[level].append(song_info.id) 166 | if version in ['舞霸']: 167 | if len(song_info.level) == 5: 168 | level= song_info.level[4] 169 | default_song_list[level].append(song_info.id) 170 | music_data_hight = 42 171 | for music_list in default_song_list.values(): 172 | 173 | item = len(music_list) 174 | if item > 0: 175 | music_data_hight = music_data_hight+38 176 | music_data_hight = music_data_hight+(127*calculate(item,10)) 177 | 178 | music_data_hight = music_data_hight-38 179 | music_data_img = Image.new("RGBA",(1420,music_data_hight),(255,255,255,0)) 180 | 181 | ix = 216 182 | iy = 42 183 | 184 | basic_img = Image.open(f"{NEW_COMPLETION_STATUS_TABLE_PATH}/song/C_Basic.png").convert('RGBA') 185 | advanced_img = Image.open(f"{NEW_COMPLETION_STATUS_TABLE_PATH}/song/C_Advanced.png").convert('RGBA') 186 | expert_img = Image.open(f"{NEW_COMPLETION_STATUS_TABLE_PATH}/song/C_Expert.png").convert('RGBA') 187 | master_img = Image.open(f"{NEW_COMPLETION_STATUS_TABLE_PATH}/song/C_Master.png").convert('RGBA') 188 | 189 | fullcombo_img = Image.open(f"{NEW_COMPLETION_STATUS_TABLE_PATH}/song/极.png").convert('RGBA') 190 | sss_img = Image.open(f"{NEW_COMPLETION_STATUS_TABLE_PATH}/song/将.png").convert('RGBA') 191 | allpefect_img = Image.open(f"{NEW_COMPLETION_STATUS_TABLE_PATH}/song/神.png").convert('RGBA') 192 | fdx_img = Image.open(f"{NEW_COMPLETION_STATUS_TABLE_PATH}/song/舞.png").convert('RGBA') 193 | confirm_img = Image.open(f"{NEW_COMPLETION_STATUS_TABLE_PATH}/song/确认.png").convert('RGBA') 194 | 195 | diff_music_data = { 196 | "total":0, 197 | "basic":0, 198 | "advanced":0, 199 | "expert":0, 200 | "master":0, 201 | } 202 | 203 | difficulty_num = 0 204 | 205 | plate_mode_img_list = { 206 | "极":fullcombo_img, 207 | "将":sss_img, 208 | "神":allpefect_img, 209 | "舞舞":fdx_img 210 | } 211 | 212 | diff_img_list = { 213 | 0:basic_img, 214 | 1:advanced_img, 215 | 2:expert_img, 216 | 3:master_img 217 | } 218 | 219 | 220 | 221 | 222 | for item in default_song_list.items(): 223 | if item[1]: 224 | count = 0 225 | rcount = 0 226 | 227 | level_log_img = Image.open(f"{NEW_COMPLETION_STATUS_TABLE_PATH}/level/{item[0]}.png").convert('RGBA') 228 | music_data_img.paste(level_log_img,(40,iy),level_log_img) 229 | for song in range(0,len(default_song_list[item[0]])): 230 | id = default_song_list[item[0]][song] 231 | 232 | music_box = Image.new("RGBA",(113,125),(255,255,255,0)) 233 | diff_list = ['basic','advanced','expert','master'] 234 | 235 | diff_music_data['total'] = diff_music_data['total']+1 236 | 237 | diff_all_completion = True 238 | for level_index in [0,1,2,3]: 239 | check_result = check_completion_conditions(plate_mode,version_song_info.get(str(id),{}).get(level_index,{})) 240 | if check_result: 241 | diff_music_data[diff_list[level_index]] = diff_music_data[diff_list[level_index]]+1 242 | completion_img = diff_img_list[level_index] 243 | music_box.paste(completion_img,(0,0),completion_img) 244 | else: 245 | diff_all_completion = False 246 | 247 | if diff_all_completion: 248 | plate_completion_img = plate_mode_img_list[plate_mode] 249 | music_box.paste(plate_completion_img,(0,0),plate_completion_img) 250 | else: 251 | if check_completion_conditions(plate_mode,version_song_info.get(str(id),{}).get(3,{})): 252 | plate_completion_img = plate_mode_img_list[plate_mode] 253 | music_box.paste(plate_completion_img,(0,0),plate_completion_img) 254 | # music_box.paste(confirm_img,(0,0),confirm_img) 255 | else: 256 | if total_list.by_id(str(id)).ds[3] >= 13.7: 257 | difficulty_num = difficulty_num+1 258 | 259 | music_data_img.paste(music_box,(ix,iy),music_box) 260 | count = count + 1 261 | rcount = rcount + 1 262 | ix = ix + 113 263 | if count == 10 and len(default_song_list[item[0]])!= rcount: 264 | ix = 216 265 | iy = iy+127 266 | count = 0 267 | iy = iy + 127+38 268 | ix = 216 269 | # p = "版本牌子完成表-抽象封面" if is_abstract else "版本牌子完成表-标准封面" 270 | # music_data_img.save(f"{COMPLETION_STATUS_TABLE_PATH}/test/{version}.png") 271 | # none_bg.show() 272 | 273 | return music_data_img,diff_music_data,difficulty_num 274 | 275 | def tran_plate_name(plate_name:str): 276 | for k,v in TRADITIONAL2SIMPLIFIED.items(): 277 | plate_name = plate_name.replace(k,v) 278 | return plate_name 279 | 280 | def generate_user_data(version:str,plate_mode:str,is_abstract:bool,userConfig,uid): 281 | try: 282 | if is_abstract: 283 | base_bg_path = f"/new_pcst_base_img_abstract/{version}.png" 284 | else: 285 | base_bg_path = f"/new_pcst_base_img_normal/{version}.png" 286 | base_img = Image.open(COMPLETION_STATUS_TABLE_PATH + base_bg_path).convert('RGBA') 287 | except: 288 | base_img = generate_full_version(version,is_abstract) 289 | 290 | # 姓名框 291 | plate_name = tran_plate_name(version+plate_mode) 292 | try: 293 | plate_id = MAIPLATE_FILE_ID_MAP[plate_name] 294 | plateImage = Image.open(PLATE_PATH + f"/UI_Plate_{plate_id}.png").convert('RGBA') 295 | except: 296 | return f"错误的文件:{plate_name}" 297 | plateImage = plateImage.resize((1314,212)) 298 | 299 | base_img.paste(plateImage,(143,161),plateImage.split()[3]) 300 | # 头像 301 | iconImage = Image.open(ICON_PATH + f'/UI_Icon_{userConfig.get("icon","000011")}.png').convert('RGBA') 302 | iconImage = iconImage.resize((198,198)) 303 | base_img.paste(iconImage,(150,168),iconImage.split()[3]) 304 | 305 | user_music_data_img,diff_music_data,difficulty_num = generate_user_music_data(version,plate_mode,uid) 306 | base_img.paste(user_music_data_img,(90,107+700),user_music_data_img.split()[3]) 307 | 308 | # text 309 | lst = list(diff_music_data.values()) 310 | 311 | if all(x == lst[0] for x in lst): 312 | text_img = Image.open(f"{NEW_COMPLETION_STATUS_TABLE_PATH}/text/已完成.png").convert('RGBA') 313 | base_img.paste(text_img,(143,402),text_img.split()[3]) 314 | else: 315 | text_img = Image.open(f"{NEW_COMPLETION_STATUS_TABLE_PATH}/text/未完成.png").convert('RGBA') 316 | textImageDraw = ImageDraw.Draw(text_img) 317 | 318 | # residue_master 319 | residue_master = str(difficulty_num) 320 | if len(residue_master) == 3: 321 | tempFont = ImageFont.truetype(FONT_PATH + "/江城圆体 700W.ttf", 45, encoding='utf-8') 322 | else: 323 | tempFont = ImageFont.truetype(FONT_PATH + "/江城圆体 700W.ttf", 53, encoding='utf-8') 324 | fx,fy = tempFont.getsize(residue_master) 325 | textImageDraw.text((265 - (fx/2), 97 - (fy/2)), residue_master, font=tempFont, fill=(255,170,198)) 326 | 327 | residue_music = sum(list(diff_music_data.values())[0]-count for count in diff_music_data.values()) 328 | single_play_count = calculate(residue_music,3) 329 | double_play_count = calculate(residue_music,4) 330 | 331 | tempFont = ImageFont.truetype(FONT_PATH + "/江城圆体 700W.ttf", 43, encoding='utf-8') 332 | fx,fy = tempFont.getsize(str(single_play_count)) 333 | textImageDraw.text((372 - (fx/2), 153- (fy/2)), str(single_play_count), font=tempFont, fill=(255,170,198)) 334 | fx,fy = tempFont.getsize(str(double_play_count)) 335 | textImageDraw.text((280 - (fx/2), 206 - (fy/2)), str(double_play_count), font=tempFont, fill=(255,170,198)) 336 | 337 | if plate_mode != "舞舞": 338 | hour = (double_play_count*12) // 60 339 | min = (double_play_count*12) % 60 340 | else: 341 | hour = (double_play_count*16) // 60 342 | min = (double_play_count*16) % 60 343 | fx,fy = tempFont.getsize(str(hour)) 344 | textImageDraw.text((193 - (fx/2), 258 - (fy/2)), str(hour), font=tempFont, fill=(255,170,198)) 345 | fx,fy = tempFont.getsize(str(min)) 346 | textImageDraw.text((328 - (fx/2), 258 - (fy/2)), str(min), font=tempFont, fill=(255,170,198)) 347 | base_img.paste(text_img,(143,402),text_img.split()[3]) 348 | 349 | 350 | # 进度条 351 | all_progress = generate_all_progress(diff_music_data['total']*4,sum([item[1] for item in diff_music_data.items() if item[0] in ['basic','advanced','expert','master']])) 352 | base_img.paste(all_progress,(602,402),all_progress) 353 | far_map = { 354 | 0:(603,526-13), 355 | 1:(1028,526-13), 356 | 2:(603,635-13), 357 | 3:(1028,635-13), 358 | } 359 | for index,diff in enumerate(['basic','advanced','expert','master']): 360 | far_progress = generate_progress(diff_music_data['total'],diff_music_data[diff],index) 361 | base_img.paste(far_progress,far_map[index],far_progress) 362 | 363 | # base_img.show() 364 | base_img = base_img.convert("RGB") 365 | return base_img 366 | 367 | 368 | 369 | 370 | 371 | def generate_progress(total:int,num:int,diff): 372 | diff_list = ["Fra_Basic","Fra_Advanced","Fra_Expert","Fra_Master"] 373 | progress_img = Image.open(f"{NEW_COMPLETION_STATUS_TABLE_PATH}/progress/{diff_list[diff]}.png").convert('RGBA') 374 | completion_rate = (total-num)/total 375 | p_w = 393 376 | un_completion_width = int(p_w*completion_rate) 377 | full_version_img = Image.new("RGBA",(un_completion_width,78),(11,13,25,255)) 378 | progress_img.paste(full_version_img,(404-un_completion_width,24),full_version_img.split()[3]) 379 | if completion_rate != 0: 380 | radius_img = Image.open(f"{NEW_COMPLETION_STATUS_TABLE_PATH}/progress/radius.png").convert('RGBA') 381 | progress_img.paste(radius_img,(404-un_completion_width-3,24),radius_img.split()[3]) 382 | 383 | fra_count_img = Image.open(f"{NEW_COMPLETION_STATUS_TABLE_PATH}/progress/Fra_Count.png").convert('RGBA') 384 | progress_img.paste(fra_count_img,(0,0),fra_count_img.split()[3]) 385 | progressImageDraw = ImageDraw.Draw(progress_img) 386 | tempFont = ImageFont.truetype(FONT_PATH + "/FOT-RaglanPunchStd-UB.otf", 32, encoding='utf-8') 387 | p = str(int((num/total)*100)) 388 | fx,fy = tempFont.getsize(str(p)+"%") 389 | progressImageDraw.text((126 - (fx/2), 62 - (fy/2)), str(p)+"%", font=tempFont, fill="white") 390 | 391 | tempFont = ImageFont.truetype(FONT_PATH + "/FOT-RaglanPunchStd-UB.otf", 25, encoding='utf-8') 392 | fx,fy = tempFont.getsize(str(num)) 393 | progressImageDraw.text((213 - (fx/2), 63 - (fy/2)), str(num), font=tempFont, fill="white") 394 | fx,fy = tempFont.getsize(str(total)) 395 | progressImageDraw.text((289 - (fx/2), 63 - (fy/2)), str(total), font=tempFont, fill="white") 396 | else: 397 | fra_clear_img = Image.open(f"{NEW_COMPLETION_STATUS_TABLE_PATH}/progress/Fra_Clear.png").convert('RGBA') 398 | progress_img.paste(fra_clear_img,(0,0),fra_clear_img.split()[3]) 399 | return progress_img 400 | 401 | def generate_all_progress(total:int,num:int): 402 | progress_img = Image.open(f"{NEW_COMPLETION_STATUS_TABLE_PATH}/progress/All_Backpack.png").convert('RGBA') 403 | completion_rate = (total-num)/total 404 | p_w = 818 405 | un_completion_width = int(p_w*completion_rate) 406 | full_version_img = Image.new("RGBA",(un_completion_width,78),(11,13,25,255)) 407 | progress_img.paste(full_version_img,(829-un_completion_width,24),full_version_img.split()[3]) 408 | if completion_rate != 0: 409 | radius_img = Image.open(f"{NEW_COMPLETION_STATUS_TABLE_PATH}/progress/radius.png").convert('RGBA') 410 | progress_img.paste(radius_img,(829-un_completion_width-3,24),radius_img.split()[3]) 411 | 412 | all_count_img = Image.open(f"{NEW_COMPLETION_STATUS_TABLE_PATH}/progress/All_Count.png").convert('RGBA') 413 | progress_img.paste(all_count_img,(0,0),all_count_img.split()[3]) 414 | progressImageDraw = ImageDraw.Draw(progress_img) 415 | tempFont = ImageFont.truetype(FONT_PATH + "/FOT-RaglanPunchStd-UB.otf", 28, encoding='utf-8') 416 | p = str(int((num/total)*100)) 417 | fx,fy = tempFont.getsize(str(p)+"%") 418 | progressImageDraw.text((598 - (fx/2), 62 - (fy/2)), str(p)+"%", font=tempFont, fill="white") 419 | 420 | tempFont = ImageFont.truetype(FONT_PATH + "/FOT-RaglanPunchStd-UB.otf", 25, encoding='utf-8') 421 | fx,fy = tempFont.getsize(str(num)) 422 | progressImageDraw.text((685 - (fx/2), 63 - (fy/2)), str(num), font=tempFont, fill="white") 423 | fx,fy = tempFont.getsize(str(total)) 424 | progressImageDraw.text((761 - (fx/2), 63 - (fy/2)), str(total), font=tempFont, fill="white") 425 | else: 426 | all_clear_img = Image.open(f"{NEW_COMPLETION_STATUS_TABLE_PATH}/progress/All_Clear.png").convert('RGBA') 427 | progress_img.paste(all_clear_img,(0,0),all_clear_img.split()[3]) 428 | 429 | return progress_img 430 | 431 | 432 | 433 | 434 | 435 | # userConfig = userdata.getUserData("381268035") 436 | # print(userConfig) 437 | # icon = userConfig.get('icon','000011') 438 | 439 | 440 | # for version in VERSION_DF_MAP.keys(): 441 | # if version in "霸舞双": 442 | # continue 443 | 444 | # generate_user_data("晓","将",False,userConfig) 445 | 446 | # generate_user_data -------------------------------------------------------------------------------- /src/libraries/maimai/completion_status_table/user_level_completion_status_table.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from PIL import Image, ImageDraw 3 | import json 4 | import requests 5 | from typing import Dict, List, Optional 6 | from src.libraries.GLOBAL_CONSTANT import ALL_VERSION_LIST 7 | from src.libraries.GLOBAL_PATH import COMPLETION_STATUS_TABLE_PATH 8 | 9 | def make_level_ds_jsonfile(): 10 | with open(f"{COMPLETION_STATUS_TABLE_PATH}/level_ds_list.json", 'r', encoding='utf-8') as fw: 11 | level_ds_list = json.load(fw) 12 | 13 | return level_ds_list 14 | 15 | level_ds_list = make_level_ds_jsonfile() 16 | 17 | def makeImg(level_str,level_ds_list:Dict,is_abstract): 18 | sssp = Image.open(f"{COMPLETION_STATUS_TABLE_PATH}/sssp.png").convert('RGBA') 19 | sss = Image.open(f"{COMPLETION_STATUS_TABLE_PATH}/sss.png").convert('RGBA') 20 | ssp = Image.open(f"{COMPLETION_STATUS_TABLE_PATH}/ssp.png").convert('RGBA') 21 | ss = Image.open(f"{COMPLETION_STATUS_TABLE_PATH}/ss.png").convert('RGBA') 22 | sp = Image.open(f"{COMPLETION_STATUS_TABLE_PATH}/sp.png").convert('RGBA') 23 | s = Image.open(f"{COMPLETION_STATUS_TABLE_PATH}/s.png").convert('RGBA') 24 | tabel_mode = "难度完成表-抽象封面" if is_abstract else "难度完成表-标准封面" 25 | img = Image.open(f"{COMPLETION_STATUS_TABLE_PATH}/{tabel_mode}/{level_str}.png").convert('RGBA') 26 | 27 | ix = 150 28 | iy = 180 29 | 30 | for ds in level_ds_list: # 不同定数有多少首歌 31 | count = 0 32 | rcount = 0 33 | for music in level_ds_list[ds]: 34 | 35 | if not music.get('achievements',0): 36 | music['achievements'] = 0 37 | if music['achievements']>= 100.5: 38 | img.paste(sssp, (ix, iy), sssp) 39 | elif music['achievements']>= 100: 40 | img.paste(sss, (ix, iy), sss) 41 | elif music['achievements']>= 99.5: 42 | img.paste(ssp, (ix, iy), ssp) 43 | elif music['achievements']>= 99: 44 | img.paste(ss, (ix, iy), ss) 45 | elif music['achievements']>= 98: 46 | img.paste(sp, (ix, iy), sp) 47 | elif music['achievements']>= 97: 48 | img.paste(s, (ix, iy), s) 49 | 50 | count = count + 1 51 | rcount = rcount + 1 52 | ix = ix + 80 53 | # print(len(level_ds_list[ds]),rcount) 54 | if count == 15 and len(level_ds_list[ds]) != rcount: 55 | ix = 150 56 | iy = iy+85 57 | count = 0 58 | iy = iy + 100 59 | ix = 150 60 | return img 61 | # print(level_str) 62 | # img.save(f"level_finish/{level_str}.png") 63 | 64 | 65 | def query_user_plate_data(QQ:str): 66 | payload = {'qq':QQ,'version':ALL_VERSION_LIST} 67 | r = requests.post("https://www.diving-fish.com/api/maimaidxprober/query/plate", json=payload) 68 | finishs = r.json() 69 | return finishs['verlist'] 70 | 71 | def generate_user_level_cst(level:str,QQ:str,is_abstract): 72 | user_music_data = query_user_plate_data(QQ) 73 | temp_level_ds_list = make_level_ds_jsonfile() 74 | level_music = temp_level_ds_list[level] 75 | for ds in level_music: 76 | for music in level_music[ds]: 77 | # print(music) 78 | for user_data in user_music_data: 79 | if str(user_data['id']) == music['song_id'] and user_data['level_index'] == music['level_index']: 80 | music['achievements'] = user_data['achievements'] 81 | return makeImg(level,level_music,is_abstract) 82 | -------------------------------------------------------------------------------- /src/libraries/maimai/completion_status_table/user_version_completion_status_table.py: -------------------------------------------------------------------------------- 1 | from src.libraries.maimai.maimaidx_music import total_list 2 | import requests 3 | from PIL import Image, ImageDraw 4 | from src.libraries.GLOBAL_CONSTANT import VERSION_MAP,VERSION_DF_MAP,DELETED_MUSIC 5 | from src.libraries.GLOBAL_PATH import COMPLETION_STATUS_TABLE_PATH 6 | 7 | def get_music_info(finishs,id:int,level:int): 8 | result = finishs['verlist'] 9 | for item in result: 10 | if item['id'] == id and item['level_index'] == level: 11 | return {'fc':item['fc'],'fs':item['fs'],'achievements':item['achievements']} 12 | return {} 13 | 14 | 15 | def query_user_plate_data(versions,user_id:str): 16 | version_list = total_list.by_version_for_plate(versions) 17 | payload = {'qq':user_id,'version':versions} 18 | r = requests.post("https://www.diving-fish.com/api/maimaidxprober/query/plate", json=payload) 19 | finishs = r.json() 20 | version_song_info = {} 21 | for song in version_list: 22 | version_song_info[song.id] = {} 23 | for index ,level in enumerate(song['level']): 24 | version_song_info[song.id][index] = get_music_info(finishs,int(song.id),index) 25 | return version_song_info 26 | 27 | def generate_user_version_plate_cst(version:str,mode:str,QQ:str,is_abstract): 28 | result_list = total_list.by_versions_for_cn(VERSION_MAP[version]) 29 | version_song_info = query_user_plate_data(VERSION_DF_MAP[version],QQ) 30 | default_song_list = { 31 | "15":[], 32 | "14+":[], 33 | "14":[], 34 | "13+":[], 35 | "13":[], 36 | "12+":[], 37 | "12":[], 38 | "11+":[], 39 | "11":[], 40 | "10+":[], 41 | "10":[], 42 | "9+":[], 43 | "9":[], 44 | "8+":[], 45 | "8":[], 46 | "7+":[], 47 | "7":[], 48 | "6":[], 49 | "5":[], 50 | "4":[], 51 | "3":[], 52 | "2":[], 53 | "1":[], 54 | } 55 | for song_info in result_list: 56 | if int(song_info.id) in DELETED_MUSIC: 57 | continue 58 | level= song_info.level[3] 59 | default_song_list[level].append(song_info.id) 60 | p = "版本牌子完成表-抽象封面" if is_abstract else "版本牌子完成表-标准封面" 61 | img = Image.open(f"{COMPLETION_STATUS_TABLE_PATH}/{p}/{version}.png").convert('RGBA') 62 | ji = Image.open(f"{COMPLETION_STATUS_TABLE_PATH}/极.png").convert('RGBA') 63 | jiang = Image.open(f"{COMPLETION_STATUS_TABLE_PATH}/将.png").convert('RGBA') 64 | shen = Image.open(f"{COMPLETION_STATUS_TABLE_PATH}/神.png").convert('RGBA') 65 | wu = Image.open(f"{COMPLETION_STATUS_TABLE_PATH}/舞舞.png").convert('RGBA') 66 | ix = 130 67 | iy = 120 68 | for item in default_song_list.items(): 69 | if item[1]: 70 | count = 0 71 | rcount = 0 72 | for song in range(0,len(default_song_list[item[0]])): 73 | id = default_song_list[item[0]][song] 74 | 75 | if version_song_info[id][3]: 76 | if mode == '将' and version_song_info[id][3]['achievements'] >= 100: 77 | if version_song_info[id][3]['fc'] in ['ap','app']: 78 | img.paste(shen, (ix, iy), shen) 79 | else: 80 | img.paste(jiang, (ix, iy), jiang) 81 | elif mode == '极' and version_song_info[id][3]['fc'] in ['fc','ap','fcp','app']: 82 | if version_song_info[id][3]['fc'] in ['ap','app']: 83 | img.paste(shen, (ix, iy), shen) 84 | else: 85 | img.paste(ji, (ix, iy), ji) 86 | elif mode == '神'and version_song_info[id][3]['fc'] in ['ap','app']: 87 | img.paste(shen, (ix, iy), shen) 88 | elif mode == '舞舞' and version_song_info[id][3]['fs'] in ['fsd','fsdp']: 89 | img.paste(wu, (ix, iy), wu) 90 | else: 91 | pass 92 | count = count + 1 93 | rcount = rcount + 1 94 | ix = ix + 80 95 | if count == 10 and len(default_song_list[item[0]])!= rcount: 96 | ix = 130 97 | iy = iy+85 98 | count = 0 99 | iy = iy + 100 100 | ix = 130 101 | return img -------------------------------------------------------------------------------- /src/libraries/maimai/completion_status_table/utils.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import requests 3 | from PIL import Image 4 | from io import BytesIO 5 | from src.libraries.GLOBAL_PATH import NORMAL_COVER_PATH,ABSTRACT_COVER_PATH 6 | from src.libraries.maimai.utils import check_file_and_save_img 7 | 8 | def get_nomal_cover_path(music_id): 9 | cover_path = Path(f"{NORMAL_COVER_PATH}/{music_id}.png") 10 | if cover_path.exists(): 11 | return cover_path 12 | else: 13 | music_id = int(int(music_id)-10000) 14 | cover_path = Path(f"{NORMAL_COVER_PATH}/{music_id}.png") 15 | if cover_path.exists(): 16 | return cover_path 17 | else: 18 | music_id = int(int(music_id)+20000) 19 | cover_path = Path(f"{NORMAL_COVER_PATH}/{music_id}.png") 20 | if cover_path.exists(): 21 | return cover_path 22 | else: 23 | raise Exception('未找到封面',music_id) 24 | 25 | 26 | 27 | def get_abstract_cover_path(music_id,abstract_cover_file_map): 28 | cover_path = abstract_cover_file_map.get(str(music_id),f"{str(music_id)}_1") 29 | 30 | img_path = Path(f"{ABSTRACT_COVER_PATH}/{cover_path}.png") 31 | check_file_and_save_img(cover_path) 32 | if img_path.exists(): 33 | return img_path 34 | else: 35 | return get_nomal_cover_path(int(music_id)) -------------------------------------------------------------------------------- /src/libraries/maimai/completion_status_table/version_image.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from PIL import Image, ImageDraw,ImageFont 3 | from .utils import get_abstract_cover_path,get_nomal_cover_path 4 | from src.libraries.maimai.maimaidx_music import total_list 5 | from src.libraries.data_handle.abstract_db_handle import abstract 6 | from src.libraries.GLOBAL_CONSTANT import VERSION_MAP,DELETED_MUSIC 7 | from src.libraries.GLOBAL_PATH import FONT_PATH,COMPLETION_STATUS_TABLE_PATH 8 | def calculate(num): 9 | if num % 10 == 0: 10 | return num // 10 11 | else: 12 | return num // 10 + 1 13 | 14 | def generate_version_image(version:str,is_abstract:bool): 15 | result_list = total_list.by_versions_for_cn(VERSION_MAP[version]) 16 | default_song_list = { 17 | "15":[], 18 | "14+":[], 19 | "14":[], 20 | "13+":[], 21 | "13":[], 22 | "12+":[], 23 | "12":[], 24 | "11+":[], 25 | "11":[], 26 | "10+":[], 27 | "10":[], 28 | "9+":[], 29 | "9":[], 30 | "8+":[], 31 | "8":[], 32 | "7+":[], 33 | "7":[], 34 | "6":[], 35 | "5":[], 36 | "4":[], 37 | "3":[], 38 | "2":[], 39 | "1":[], 40 | } 41 | 42 | h = 180 43 | 44 | for song_info in result_list: 45 | if int(song_info.id) in DELETED_MUSIC: 46 | continue 47 | level= song_info.level[3] 48 | default_song_list[level].append(song_info.id) 49 | 50 | for music_list in default_song_list.values(): 51 | item = len(music_list) 52 | if item > 0: 53 | h = h+ (85*calculate(item)) 54 | none_bg = Image.new("RGBA",(1000,h+100),(255,255,255,0)) 55 | bg_img = Image.open(f"{COMPLETION_STATUS_TABLE_PATH}/版本完成表背景/{version}.png").convert('RGBA') 56 | img = bg_img 57 | if version in ["祭","祝"]: 58 | none_bg.paste(bg_img,(0,0),bg_img) 59 | img = none_bg 60 | 61 | draw = ImageDraw.Draw(img) 62 | 63 | ix = 130 64 | iy = 120 65 | 66 | if is_abstract: 67 | abstract_cover_file_map = abstract.get_abstract_file_name_all() 68 | 69 | fontstyle = ImageFont.truetype(f'{FONT_PATH}/SourceHanSans_17.ttf',40) 70 | 71 | for item in default_song_list.items(): 72 | if item[1]: 73 | count = 0 74 | rcount = 0 75 | content = str(item[0]) 76 | # 计算文本的尺寸 77 | x0,yo = fontstyle.getsize(content) 78 | # 绘制文本描边 79 | outline_color = "black" 80 | draw.text((60-(x0/2)-1, iy-1), content, font=fontstyle, fill=outline_color) # 左上 81 | draw.text((60-(x0/2)+1, iy-1), content, font=fontstyle, fill=outline_color) # 右上 82 | draw.text((60-(x0/2)-1, iy+1), content, font=fontstyle, fill=outline_color) # 左下 83 | draw.text((60-(x0/2)+1, iy+1), content, font=fontstyle, fill=outline_color) # 右下 84 | # 绘制文本 85 | text_color = "white" 86 | draw.text((60-(x0/2), iy), content, font=fontstyle, fill=text_color) 87 | 88 | for song in range(0,len(default_song_list[item[0]])): 89 | id = default_song_list[item[0]][song] 90 | 91 | if is_abstract: 92 | cover_path = get_abstract_cover_path(int(id),abstract_cover_file_map) 93 | else: 94 | cover_path = get_nomal_cover_path(int(id)) 95 | cover = Image.open(cover_path).convert('RGBA') 96 | 97 | cover = cover.resize((70, 70), Image.ANTIALIAS) 98 | img.paste(cover, (ix, iy), cover) 99 | 100 | count = count + 1 101 | rcount = rcount + 1 102 | ix = ix + 80 103 | if count == 10 and len(default_song_list[item[0]])!= rcount: 104 | ix = 130 105 | iy = iy+85 106 | count = 0 107 | iy = iy + 100 108 | ix = 130 109 | p = "版本牌子完成表-抽象封面" if is_abstract else "版本牌子完成表-标准封面" 110 | img.save(f"{COMPLETION_STATUS_TABLE_PATH}/{p}/{version}.png") 111 | 112 | return True -------------------------------------------------------------------------------- /src/libraries/maimai/maimai_fortune/maimai_fortune.py: -------------------------------------------------------------------------------- 1 | from PIL import Image,ImageFont,ImageDraw 2 | import os 3 | from src.libraries.data_handle.abstract_db_handle import abstract 4 | from src.libraries.GLOBAL_PATH import FORTUNE_PATH,FONT_PATH,NORMAL_COVER_PATH 5 | from src.libraries.maimai.utils import get_abstract_cover_path_by_file_id 6 | 7 | def _getCharWidth(o) -> int: 8 | widths = [ 9 | (126, 1), (159, 0), (687, 1), (710, 0), (711, 1), (727, 10 | 0), (733, 1), (879, 0), (1154, 1), (1161, 0), 11 | (4347, 1), (4447, 2), (7467, 1), (7521, 0), (8369, 12 | 1), (8426, 0), (9000, 1), (9002, 2), (11021, 1), 13 | (12350, 2), (12351, 1), (12438, 2), (12442, 14 | 0), (19893, 2), (19967, 1), (55203, 2), (63743, 1), 15 | (64106, 2), (65039, 1), (65059, 0), (65131, 16 | 2), (65279, 1), (65376, 2), (65500, 1), (65510, 2), 17 | (120831, 1), (262141, 2), (1114109, 1), 18 | ] 19 | if o == 0xe or o == 0xf: 20 | return 0 21 | for num, wid in widths: 22 | if o <= num: 23 | return wid 24 | return 1 25 | 26 | def _coloumWidth(s: str): 27 | res = 0 28 | for ch in s: 29 | res += _getCharWidth(ord(ch)) 30 | return res 31 | 32 | def _changeColumnWidth(s: str, len: int) -> str: 33 | res = 0 34 | sList = [] 35 | for ch in s: 36 | res += _getCharWidth(ord(ch)) 37 | if res <= len: 38 | sList.append(ch) 39 | return ''.join(sList) 40 | 41 | def makeFortune(year:str,month:str,day:str,week:str,lucky:str,should,noshould,music): 42 | fortune = Image.new('RGB',(1080,1248),(255,255,255)) 43 | 44 | 45 | if int(music.id) in abstract.get_abstract_id_list(): 46 | file_name,nickname = abstract.get_abstract_file_name(str(music.id)) 47 | file_path = get_abstract_cover_path_by_file_id(file_name) 48 | cover = Image.open(file_path) 49 | else: 50 | if os.path.exists(f'{NORMAL_COVER_PATH}/{(music.id)}.png'): 51 | cover = Image.open(f'{NORMAL_COVER_PATH}/{(music.id)}.png') 52 | else: 53 | cover = Image.open(f'{NORMAL_COVER_PATH}/{(str(int(music.id)-10000))}.png') 54 | # cover = Image.open('388.png') 55 | cover = cover.resize((366,366)) 56 | fortune.paste(cover,(548,827)) 57 | Bg = Image.open(f'{FORTUNE_PATH}/背景.png').convert('RGBA') 58 | fortune.paste(Bg,(0,0),Bg.split()[3]) 59 | draw = ImageDraw.Draw(fortune) 60 | fontstyle = ImageFont.truetype(f'{FONT_PATH}/隶书.TTF',48) 61 | draw.text((329,188),week,(224,88,76),fontstyle) 62 | fontstyle = ImageFont.truetype(f'{FONT_PATH}/华文行楷.TTF',48) 63 | draw.text((63,136),f'{year}年{str(int(month))}月',(224,88,76),fontstyle) 64 | fontstyle = ImageFont.truetype(f'{FONT_PATH}/华文行楷.TTF',111) 65 | if len(day) == 1: 66 | draw.text((128,211),day,(224,88,76),fontstyle) 67 | else: 68 | draw.text((113,211),day,(224,88,76),fontstyle) 69 | fontstyle = ImageFont.truetype(f'{FONT_PATH}/华文行楷.TTF',85) 70 | if len(lucky) == 1: 71 | draw.text((425,416),lucky,(232,225,206),fontstyle) 72 | else: 73 | draw.text((405,416),lucky,(232,225,206),fontstyle) 74 | fontstyle = ImageFont.truetype(f'{FONT_PATH}/华文行楷.TTF',42) 75 | 76 | if len(should) == 1: 77 | draw.text((72,706),should[0],(224,88,76),fontstyle) 78 | else: 79 | draw.text((72,706),should[0],(224,88,76),fontstyle) 80 | draw.text((72,756),should[1],(224,88,76),fontstyle) 81 | 82 | if len(noshould) == 1: 83 | draw.text((72,1011),noshould[0],(224,88,76),fontstyle) 84 | else: 85 | draw.text((72,1011),noshould[0],(224,88,76),fontstyle) 86 | draw.text((72,1061),noshould[1],(224,88,76),fontstyle) 87 | 88 | fontstyle = ImageFont.truetype(f'{FONT_PATH}/男朋友字体.ttf',36) 89 | title= music.title 90 | if _coloumWidth(title) > 18: 91 | title = _changeColumnWidth(title, 17) + '...' 92 | art = music.artist 93 | if _coloumWidth(art) > 17: 94 | art = _changeColumnWidth(art, 16) + '...' 95 | level = '' 96 | for d in music.level: 97 | level += str(d) + '/' 98 | level = level[:-1] 99 | ds = '' 100 | for d in music.ds: 101 | ds += str(d) + '/' 102 | ds = ds[:-1] 103 | draw.text((548,496),f'ID:{str(music.id)}\n标题:{title}\n艺术家:{art}\nBPM:{str(music.bpm)}\n分类:{str(music.genre)}\n等级:{level}\n定数:{ds}',(224,88,76),fontstyle) 104 | # fortune.show() 105 | return fortune 106 | 107 | def generate_fortune(year:str,month:str,day:str,week:str,lucky:str,should,noshould,music): 108 | fortune_img = makeFortune(year,month,day,week,lucky,should,noshould,music) 109 | return fortune_img -------------------------------------------------------------------------------- /src/libraries/maimai/maimai_fortune/utils.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import time 3 | from src.libraries.maimai.maimaidx_music import * 4 | from src.libraries.image_handle.image import * 5 | from src.libraries.GLOBAL_CONSTANT import FORTUNE_LIST 6 | from .maimai_fortune import generate_fortune 7 | 8 | 9 | def gen_fortune(event): 10 | qq = str(event.user_id) 11 | qq = int(qq.replace('0', '')) 12 | DayTime = time.strftime("%m%d", time.localtime()) 13 | h = qq * int(DayTime) 14 | rp = h % 100 15 | wm_value = [] 16 | year = time.strftime("%Y", time.localtime()) 17 | month = time.strftime("%m", time.localtime()) 18 | day = time.strftime("%d", time.localtime()) 19 | for i in range(11): 20 | wm_value.append(h & 3) 21 | h >>= 2 22 | s = f"🌈{DayTime}:幸运值🌟:{rp}!\n" 23 | should_list = [] 24 | noshould_list = [] 25 | for i in range(11): 26 | if wm_value[i] == 3: 27 | should_list.append(FORTUNE_LIST[i][0]) 28 | elif wm_value[i] == 0: 29 | noshould_list.append(FORTUNE_LIST[i][0]) 30 | week_list = ["星\n期\n一", "星\n期\n二", "星\n期\n三", "星\n期\n四", "星\n期\n五", "星\n期\n六", "星\n期\n日"] 31 | 32 | should = [] 33 | noshould = [] 34 | 35 | itemstr = '' 36 | index = 0 37 | for item in should_list: 38 | itemstr += item + ',' 39 | index += 1 40 | if index == 3: 41 | index = 0 42 | should.append(itemstr[:-1]) 43 | itemstr = '' 44 | 45 | if itemstr: 46 | should.append(itemstr[:-1]) 47 | itemstr = '' 48 | index = 0 49 | 50 | if len(should) > 2: 51 | should = should[:2] 52 | elif len(should) == 0: 53 | should = ['无'] 54 | 55 | for item in noshould_list: 56 | itemstr += item + ',' 57 | index += 1 58 | if index == 3: 59 | index = 0 60 | noshould.append(itemstr[:-1]) 61 | itemstr = '' 62 | 63 | if itemstr: 64 | noshould.append(itemstr[:-1]) 65 | itemstr = '' 66 | index = 0 67 | 68 | if len(noshould) > 2: 69 | noshould = noshould[:2] 70 | elif len(noshould) == 0: 71 | noshould = ['无'] 72 | 73 | week = week_list[datetime.datetime.today().weekday()] 74 | music = total_list[h % len(total_list)] 75 | img = generate_fortune(year, month, day, week, str(rp), should, noshould, music) 76 | return img -------------------------------------------------------------------------------- /src/libraries/maimai/maimai_music_data/maimai_music_data.py: -------------------------------------------------------------------------------- 1 | from src.libraries.image_handle.image import split_text_to_lines 2 | from PIL import Image,ImageFont,ImageDraw 3 | from src.libraries.maimai.maimaidx_music import total_list 4 | from src.libraries.maimai.utils import get_cover_path 5 | from src.libraries.GLOBAL_CONSTANT import VERSION_LOGO_MAP 6 | from src.libraries.execution_time import timing_decorator 7 | from .utils import truncate_text,decimalPoints 8 | from src.libraries.data_handle.abstract_db_handle import abstract 9 | from src.libraries.GLOBAL_PATH import MAIMAI_MUSIC_DATA,FONT_PATH 10 | from src.libraries.maimai.utils import get_abstract_cover_path_by_file_id 11 | class MaiMusicData(): 12 | def __init__(self,music_id:int,is_abstract:bool) -> None: 13 | self.music_id = music_id 14 | self.music_info = total_list.by_id(str(music_id)) 15 | self.is_abstract = is_abstract 16 | 17 | self.baseImg = Image.new("RGBA", (1700, 2000), (0, 0, 0, 0)) 18 | self.baseImgDraw = ImageDraw.Draw(self.baseImg) 19 | 20 | 21 | @timing_decorator 22 | def draw(self): 23 | if self.is_abstract: 24 | abstract_data = abstract.get_abstract_id_list() 25 | if int(self.music_id) in abstract_data: 26 | file_name,nickname = abstract.get_abstract_file_name(str(self.music_id)) 27 | filename = file_name 28 | self.abstract_artist = nickname 29 | musicCoverImg = Image.open(get_abstract_cover_path_by_file_id(file_name)).convert('RGBA') 30 | else: 31 | self.abstract_artist = "抽象画未收录" 32 | musicCoverImg = Image.open(get_cover_path(self.music_id,False)).convert('RGBA') 33 | else: 34 | self.abstract_artist = "-" 35 | musicCoverImg = Image.open(get_cover_path(self.music_id,False)).convert('RGBA') 36 | musicCoverImg = musicCoverImg.resize((494,494)) 37 | self.baseImg.paste(musicCoverImg,(191,172),musicCoverImg.split()[3]) 38 | 39 | 40 | backageImg = Image.open(f"{MAIMAI_MUSIC_DATA}/background.png").convert('RGBA') 41 | self.baseImg.paste(backageImg,(0,0),backageImg.split()[3]) 42 | 43 | # 添加曲师 44 | tempFont = ImageFont.truetype(f"{FONT_PATH}/GlowSansSC-Normal-Bold.otf", 43, encoding='utf-8') 45 | artist = truncate_text(self.music_info.artist, tempFont, 800) 46 | self.baseImgDraw.text((729,180), artist , "white" , tempFont) 47 | 48 | # Title 49 | tempFont = ImageFont.truetype(f'{FONT_PATH}/SourceHanSans_35.otf', 60, encoding='utf-8') 50 | title = split_text_to_lines(self.music_info.title,800,tempFont) 51 | self.baseImgDraw.text((727,246), title , "white" , tempFont) 52 | 53 | # music_id bpm 分类 版本 54 | tempFont = ImageFont.truetype(f"{FONT_PATH}/GlowSansSC-Normal-Bold.otf", 45, encoding='utf-8') 55 | 56 | text_width, text_height = tempFont.getsize(self.music_info.id) 57 | self.baseImgDraw.text(((810-text_width/2),(610-text_height/2)), self.music_info.id , "white" , tempFont) 58 | 59 | text_width, text_height = tempFont.getsize(str(self.music_info.bpm)) 60 | self.baseImgDraw.text(((953-text_width/2),(610-text_height/2)), str(self.music_info.bpm) , "white" , tempFont) 61 | 62 | if self.music_info.genre in ["niconico & VOCALOID","niconicoボーカロイド","ゲームバラエティ"]: 63 | tempFont = ImageFont.truetype(f"{FONT_PATH}/GlowSansSC-Normal-Bold.otf", 30, encoding='utf-8') 64 | text_width, text_height = tempFont.getsize(self.music_info.genre) 65 | self.baseImgDraw.text(((1194-text_width/2),(613-text_height/2)), self.music_info.genre , "white" , tempFont) 66 | else: 67 | text_width, text_height = tempFont.getsize(self.music_info.genre) 68 | self.baseImgDraw.text(((1194-text_width/2),(614-text_height/2)), self.music_info.genre , "white" , tempFont) 69 | 70 | # 类型 71 | TypeIconImg = Image.open(f"{MAIMAI_MUSIC_DATA}/类型/{self.music_info.type}.png").convert('RGBA') 72 | self.baseImg.paste(TypeIconImg,(1393,597),TypeIconImg.split()[3]) 73 | 74 | # logo 75 | if self.music_info.cn_version in VERSION_LOGO_MAP: 76 | VersionLogoImg = Image.open(f"{MAIMAI_MUSIC_DATA}/版本牌/UI_CMN_TabTitle_MaimaiTitle_Ver{VERSION_LOGO_MAP.get(self.music_info.cn_version)}.png").convert('RGBA') 77 | text_width, text_height = VersionLogoImg.size 78 | self.baseImg.paste(VersionLogoImg,(250-int(text_width/2),150-int(text_height/2)),VersionLogoImg.split()[3]) 79 | 80 | if len(self.music_info.ds) > 4: 81 | charterBg= Image.open(f"{MAIMAI_MUSIC_DATA}/Charter_2.png").convert('RGBA') 82 | charter_map = [(),(),(247,27),(247,107),(247,187)] 83 | else: 84 | charterBg= Image.open(f"{MAIMAI_MUSIC_DATA}/Charter_1.png").convert('RGBA') 85 | charter_map = [(),(),(250,47),(250,165)] 86 | charterDraw = ImageDraw.Draw(charterBg) 87 | tempFont = ImageFont.truetype(f"{FONT_PATH}/GlowSansSC-Normal-Bold.otf", 40, encoding='utf-8') 88 | for index,chart in enumerate(self.music_info.charts): 89 | if charter_map[index]: 90 | chart_charter = str(chart['charter']) 91 | chart_charter = truncate_text(chart_charter, tempFont, 550) 92 | charterDraw.text(charter_map[index], chart_charter , "black" , tempFont) 93 | self.baseImg.paste(charterBg,(181,1429),charterBg.split()[3]) 94 | 95 | versionBg= Image.open(f"{MAIMAI_MUSIC_DATA}/Ver.png").convert('RGBA') 96 | versionDraw = ImageDraw.Draw(versionBg) 97 | tempFont = ImageFont.truetype(f"{FONT_PATH}/GlowSansSC-Normal-Bold.otf", 40, encoding='utf-8') 98 | version = truncate_text(self.music_info.ez_version, tempFont, 420) 99 | fontSizeX,fontSizeY = tempFont.getsize(version) 100 | versionDraw.text((257-int(fontSizeX/2),70), version , "black" , tempFont) 101 | abstract_artist = truncate_text(self.abstract_artist, tempFont, 420) 102 | fontSizeX,fontSizeY = tempFont.getsize(abstract_artist) 103 | versionDraw.text((257-int(fontSizeX/2),187), abstract_artist , "black" , tempFont) 104 | self.baseImg.paste(versionBg,(1005,1429),versionBg.split()[3]) 105 | 106 | if len(self.music_info.ds) > 4: 107 | TapDataBg = Image.open(f"{MAIMAI_MUSIC_DATA}/Total_2-1.png").convert('RGBA') 108 | ReMasterIcon = Image.open(f"{MAIMAI_MUSIC_DATA}/Total_2-2.png").convert('RGBA') 109 | TapDataBg.paste(ReMasterIcon,(0,0),ReMasterIcon.split()[3]) 110 | TapDataDraw = ImageDraw.Draw(TapDataBg) 111 | for index,chart in enumerate(self.music_info.charts): 112 | ds = self.music_info.ds[index] 113 | tempFont = ImageFont.truetype(f"{FONT_PATH}/RoGSanSrfStd-Bd.otf", 50, encoding='utf-8') 114 | ds = decimalPoints(ds,1) 115 | fontSizeX,fontSizeY = tempFont.getsize(ds) 116 | TapDataDraw.text((350+(202*index) - int(fontSizeX/2),100 - int(fontSizeY/2)), ds , "white" , tempFont) 117 | notes = list(chart['notes']) 118 | total_notes = sum(notes) 119 | 120 | if len(notes) == 4: 121 | notes.insert(len(notes)-1, '-') 122 | notes.insert(0, total_notes) 123 | 124 | for note_index,note_count in enumerate(notes): 125 | tempFont = ImageFont.truetype(f"{FONT_PATH}/RoGSanSrfStd-Bd.otf", 48, encoding='utf-8') 126 | note_count = str(note_count) 127 | fontSizeX,fontSizeY = tempFont.getsize(note_count) 128 | TapDataDraw.text((350+(202*index) - int(fontSizeX/2),162+(88*note_index) ), note_count , "black" , tempFont) 129 | self.baseImg.paste(TapDataBg,(206,729),TapDataBg.split()[3]) 130 | else: 131 | TapDataBg = Image.open(f"{MAIMAI_MUSIC_DATA}/Total_1.png").convert('RGBA') 132 | TapDataDraw = ImageDraw.Draw(TapDataBg) 133 | for index,chart in enumerate(self.music_info.charts): 134 | ds = self.music_info.ds[index] 135 | tempFont = ImageFont.truetype(f"{FONT_PATH}/RoGSanSrfStd-Bd.otf", 50, encoding='utf-8') 136 | ds = decimalPoints(ds,1) 137 | fontSizeX,fontSizeY = tempFont.getsize(ds) 138 | TapDataDraw.text((350+(202*index) - int(fontSizeX/2),100 - int(fontSizeY/2)), ds , "white" , tempFont) 139 | notes = list(chart['notes']) 140 | total_notes = sum(notes) 141 | 142 | if len(notes) == 4: 143 | notes.insert(len(notes)-1, '-') 144 | notes.insert(0, total_notes) 145 | 146 | for note_index,note_count in enumerate(notes): 147 | tempFont = ImageFont.truetype(f"{FONT_PATH}/RoGSanSrfStd-Bd.otf", 48, encoding='utf-8') 148 | note_count = str(note_count) 149 | fontSizeX,fontSizeY = tempFont.getsize(note_count) 150 | TapDataDraw.text((350+(202*index) - int(fontSizeX/2),162+(88*note_index) ), note_count , "black" , tempFont) 151 | self.baseImg.paste(TapDataBg,(307,729),TapDataBg.split()[3]) 152 | return self.baseImg 153 | 154 | 155 | -------------------------------------------------------------------------------- /src/libraries/maimai/maimai_music_data/maimai_utage_music_data.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module let you draw the UTAGE music data image. 3 | """ 4 | import json 5 | import re 6 | 7 | from PIL import Image, ImageFont, ImageDraw 8 | 9 | from src.libraries.GLOBAL_PATH import UTAGE_MAIMAI_MUSIC_DATA, FONT_PATH, ABSTRACT_COVER_PATH 10 | from src.libraries.data_handle.abstract_db_handle import abstract 11 | from src.libraries.execution_time import timing_decorator 12 | from src.libraries.maimai.maimai_music_data.utils import truncate_text 13 | from src.libraries.maimai.maimaidx_music import total_list 14 | from src.libraries.maimai.utils import get_cover_path 15 | 16 | 17 | class MaiUtageMusicData: 18 | """ 19 | Draw UTAGE music data image. 20 | """ 21 | 22 | def __init__(self, music_id: int, is_abstract: bool) -> None: 23 | self.abstract_artist = "" 24 | self.music_id = music_id 25 | self.music_info = total_list.by_id(str(music_id)) 26 | self.is_abstract = is_abstract 27 | self.base_img = Image.new("RGBA", (1700, 2000), (0, 0, 0, 0)) 28 | self.base_img_draw = ImageDraw.Draw(self.base_img) 29 | self.utg_music_data = self.get_utg_music_data() 30 | 31 | def get_utg_music_data(self): 32 | """ 33 | Get UTAGE music data. 34 | """ 35 | path = UTAGE_MAIMAI_MUSIC_DATA + "/utg_song_data.json" 36 | with open(path, 'r', encoding="utf-8") as file: 37 | jp_obj = json.load(file) 38 | music_data = jp_obj.get(str(self.music_id), {}) 39 | print(music_data) 40 | return music_data 41 | 42 | @timing_decorator 43 | def draw(self): 44 | """ 45 | Main function to draw the image. 46 | For UTAGE. 47 | """ 48 | self.draw_cover() 49 | self.draw_bg() 50 | self.draw_artist() 51 | self.draw_title() 52 | self.draw_utage_type() 53 | self.draw_music_id() 54 | self.draw_bpm() 55 | self.draw_type() 56 | self.draw_comment() 57 | self.draw_charter() 58 | self.draw_version() 59 | self.draw_abstract_artist() 60 | self.draw_referrals_num() 61 | self.draw_buddy() 62 | return self.base_img 63 | 64 | def draw_charter(self): 65 | """ 66 | draw the charter 67 | """ 68 | level_icon_image = ( 69 | Image.open( 70 | f"{UTAGE_MAIMAI_MUSIC_DATA}/levels/" 71 | f"{str(self.utg_music_data['level'][0]).replace('?', '')}.png" 72 | ) 73 | .convert('RGBA') 74 | ) 75 | if len(self.utg_music_data['charts']) > 1: 76 | charter_bg = Image.open( 77 | f"{UTAGE_MAIMAI_MUSIC_DATA}/Charter_2.png" 78 | ).convert('RGBA') 79 | start_y = 246 80 | charter_bg.paste( 81 | level_icon_image, 82 | (181, 118 - 49), 83 | level_icon_image 84 | ) 85 | else: 86 | charter_bg = Image.open( 87 | f"{UTAGE_MAIMAI_MUSIC_DATA}/Charter_1.png" 88 | ).convert('RGBA') 89 | start_y = 295 90 | charter_bg.paste( 91 | level_icon_image, 92 | (181, 118), 93 | level_icon_image 94 | ) 95 | charter_draw = ImageDraw.Draw(charter_bg) 96 | for data_chart in self.utg_music_data['charts']: 97 | chart_notes = list(data_chart['notes']) 98 | 99 | font = get_font("FOT-RaglanPunchStd-UB.otf", 53) 100 | _, _, text_width, text_height = font.getbbox(str(sum(chart_notes))) 101 | charter_draw.text( 102 | (482 - text_width / 2, start_y - text_height / 2), 103 | str(sum(chart_notes)), 104 | "white", 105 | font 106 | ) 107 | 108 | font = get_font("FOT-RaglanPunchStd-UB.otf", 45) 109 | for index, chart in enumerate(chart_notes): 110 | note = str(chart) 111 | _, _, text_width, text_height = font.getbbox(note) 112 | charter_draw.text( 113 | (672 + (190 * index) - text_width / 2, start_y - text_height / 2), 114 | note, 115 | "white", 116 | font 117 | ) 118 | 119 | start_y += 105 120 | self.base_img.paste( 121 | charter_bg, 122 | (0, 919), 123 | charter_bg 124 | ) 125 | 126 | def draw_referrals_num(self): 127 | """ 128 | draw the referrals num 129 | """ 130 | font = get_font("FOT-RaglanPunchStd-UB.otf", 75) 131 | referrals_num = self.utg_music_data.get('referrals_num') 132 | # single player 133 | if referrals_num['mode'] == 'normal': 134 | referrals_num_img = Image.open( 135 | f"{UTAGE_MAIMAI_MUSIC_DATA}/referrals_num/normal.png" 136 | ).convert('RGBA') 137 | self.base_img.paste( 138 | referrals_num_img, 139 | (1002, 1404), 140 | referrals_num_img.split()[3] 141 | ) 142 | player = str(referrals_num['player'][0]) 143 | _, _, text_width, text_height = font.getbbox(player) 144 | 145 | # single player draw 146 | self.base_img_draw.text( 147 | (1002 + 383 - text_width / 2, 1399 + 99 - text_height / 2), 148 | player, 149 | "white", 150 | font 151 | ) 152 | 153 | # two player 154 | elif referrals_num['mode'] == 'buddy_normal': 155 | referrals_num_img = Image.open( 156 | f"{UTAGE_MAIMAI_MUSIC_DATA}/referrals_num/buddy_normal.png").convert('RGBA') 157 | self.base_img.paste( 158 | referrals_num_img, 159 | (1002, 1404), 160 | referrals_num_img.split()[3] 161 | ) 162 | player_left = str(referrals_num['player'][0]) 163 | player_right = str(referrals_num['player'][1]) 164 | _, _, text_width, text_height = font.getbbox(player_left) 165 | 166 | # player left draw 167 | self.base_img_draw.text( 168 | (1002 + 289 - text_width / 2, 1399 + 99 - text_height / 2), 169 | player_left, 170 | "white", 171 | font 172 | ) 173 | 174 | _, _, text_width, text_height = font.getbbox(player_right) 175 | 176 | # player right draw 177 | self.base_img_draw.text( 178 | (1002 + 494 - text_width / 2, 1399 + 99 - text_height / 2), 179 | player_right, 180 | "white", 181 | font 182 | ) 183 | 184 | # three player 185 | elif referrals_num['mode'] == 'buddy_clamp': 186 | referrals_num_img = Image.open( 187 | f"{UTAGE_MAIMAI_MUSIC_DATA}/referrals_num/buddy_clamp.png" 188 | ).convert('RGBA') 189 | self.base_img.paste( 190 | referrals_num_img, 191 | (1002, 1404), 192 | referrals_num_img.split()[3] 193 | ) 194 | player_left = str(referrals_num['player'][0]) 195 | player_mid = str(referrals_num['player'][1]) 196 | player_right = str(referrals_num['player'][2]) 197 | _, _, text_width, text_height = font.getbbox(player_left) 198 | self.base_img_draw.text( 199 | (1002 + 255 - text_width / 2, 1399 + 99 - text_height / 2), 200 | player_left, 201 | "white", 202 | font 203 | ) 204 | _, _, text_width, text_height = font.getbbox(player_mid) 205 | self.base_img_draw.text( 206 | (1002 + 389 - text_width / 2, 1399 + 99 - text_height / 2), 207 | player_mid, 208 | "white", 209 | font 210 | ) 211 | _, _, text_width, text_height = font.getbbox(player_right) 212 | self.base_img_draw.text( 213 | (1002 + 525 - text_width / 2, 1399 + 99 - text_height / 2), 214 | player_right, 215 | "white", 216 | font 217 | ) 218 | 219 | def draw_abstract_artist(self): 220 | """ 221 | draw the abstract artist 222 | """ 223 | abstract_artist = self.abstract_artist 224 | font = get_font("GlowSansSC-Normal-ExtraBold.otf", 47) 225 | _, _, text_width, text_height = font.getbbox(abstract_artist) 226 | self.base_img_draw.text( 227 | ( 228 | 676 - text_width / 2, 229 | 1463 + 104 * 2 - text_height / 2 230 | ), 231 | abstract_artist, 232 | "white", 233 | font 234 | ) 235 | 236 | def draw_version(self): 237 | """ 238 | draw the version 239 | """ 240 | font = get_font("GlowSansSC-Normal-ExtraBold.otf", 47) 241 | jp_ver = self.utg_music_data.get('jp_update') 242 | cn_ver = self.utg_music_data.get('cn_update') 243 | _, _, text_width, text_height = font.getbbox(jp_ver) 244 | self.base_img_draw.text( 245 | (676 - text_width / 2, 1463 - text_height / 2), 246 | jp_ver, 247 | "white", 248 | font 249 | ) 250 | _, _, text_width, text_height = font.getbbox(cn_ver) 251 | self.base_img_draw.text( 252 | ( 253 | 676 - text_width / 2, 254 | 1463 + 104 - text_height / 2 255 | ), 256 | cn_ver, 257 | "white", 258 | font 259 | ) 260 | return font 261 | 262 | def draw_comment(self): 263 | """ 264 | Draw music comment. 265 | """ 266 | font = get_font("GlowSansSC-Normal-Bold.otf", 40) 267 | comment = self.utg_music_data.get('comment', 'LET PLAY') 268 | _, _, text_width, text_height = font.getbbox(comment) 269 | self.base_img_draw.text( 270 | (850 - text_width / 2, 840 - text_height / 2), 271 | comment, 272 | "white", 273 | font 274 | ) 275 | 276 | def draw_type(self): 277 | """ 278 | Draw chart type 279 | """ 280 | type_img = Image.open( 281 | f"{UTAGE_MAIMAI_MUSIC_DATA}/others/type_{self.utg_music_data['type'].lower()}.png" 282 | ).convert('RGBA') 283 | self.base_img.paste( 284 | type_img, 285 | (0, 0), 286 | type_img.split()[3] 287 | ) 288 | 289 | def draw_bpm(self): 290 | """ 291 | Draw music BPM. 292 | """ 293 | font = get_font("GlowSansSC-Normal-Bold.otf", 42) 294 | _, _, text_width, text_height = font.getbbox(str(self.music_info.bpm)) 295 | self.base_img_draw.text( 296 | (987 - text_width / 2, 629 - text_height / 2), 297 | str(self.music_info.bpm), 298 | "white", 299 | font 300 | ) 301 | 302 | def draw_music_id(self): 303 | """ 304 | Draw music ID. 305 | """ 306 | font = get_font("GlowSansSC-Normal-Bold.otf", 42) 307 | _, _, text_width, text_height = font.getbbox(self.music_info.id) 308 | self.base_img_draw.text( 309 | (815 - text_width / 2, 629 - text_height / 2), 310 | self.music_info.id, 311 | "white", 312 | font 313 | ) 314 | 315 | def draw_utage_type(self): 316 | """ 317 | Draw utage type. 318 | """ 319 | font = get_font("M Plus 5.ttf", 46) 320 | matches = re.findall(r'\[(\w+|\W+)]', self.music_info.title.title()) 321 | utage_type = truncate_text( 322 | matches[0] if matches else [''], 323 | font, 324 | 800 325 | ) 326 | _, _, text_width, text_height = font.getbbox(utage_type) 327 | self.base_img_draw.text( 328 | (876 - text_width / 2, 447 - text_height / 2), 329 | utage_type, 330 | "white", 331 | font 332 | ) 333 | 334 | def draw_title(self): 335 | """ 336 | Draw music title. 337 | """ 338 | font = get_font("SourceHanSans_35.otf", 60) 339 | title = truncate_text( 340 | self.music_info.title, 341 | font, 342 | 800 343 | ) 344 | self.base_img_draw.text( 345 | (727, 246), 346 | title, 347 | "white", 348 | font 349 | ) 350 | 351 | def draw_artist(self): 352 | """ 353 | Draw music artist. 354 | """ 355 | font = get_font("GlowSansSC-Normal-Bold.otf", 43) 356 | artist = truncate_text( 357 | self.music_info.artist, 358 | font, 359 | 800 360 | ) 361 | self.base_img_draw.text( 362 | (729, 180), 363 | artist, 364 | "white", 365 | font 366 | ) 367 | 368 | def draw_cover(self): 369 | """ 370 | Copy music cover image. 371 | """ 372 | if self.is_abstract: 373 | abstract_data = abstract.get_abstract_id_list() 374 | if int(self.music_id) in abstract_data: 375 | file_name, nickname = abstract.get_abstract_file_name(str(self.music_id)) 376 | filename = file_name 377 | self.abstract_artist = nickname 378 | music_cover_img = Image.open( 379 | f'{ABSTRACT_COVER_PATH}/{filename}.png' 380 | ).convert('RGBA') 381 | else: 382 | self.abstract_artist = "抽象画未收录" 383 | music_cover_img = Image.open( 384 | get_cover_path(self.music_id, False) 385 | ).convert('RGBA') 386 | else: 387 | self.abstract_artist = "-" 388 | music_cover_img = Image.open( 389 | get_cover_path(self.music_id, False) 390 | ).convert('RGBA') 391 | music_cover_img = music_cover_img.resize((494, 494)) 392 | self.base_img.paste( 393 | music_cover_img, 394 | (191, 172), 395 | music_cover_img.split()[3] 396 | ) 397 | 398 | def draw_bg(self): 399 | """ 400 | Copy background image. 401 | """ 402 | background_img = Image.open( 403 | f"{UTAGE_MAIMAI_MUSIC_DATA}/background.png" 404 | ).convert('RGBA') 405 | self.base_img.paste( 406 | background_img, 407 | (0, 0), 408 | background_img.split()[3] 409 | ) 410 | 411 | def draw_buddy(self): 412 | """ 413 | Draw buddy icon. 414 | """ 415 | if self.utg_music_data['referrals_num']['mode'] == 'buddy_normal' or \ 416 | self.utg_music_data['referrals_num']['mode'] == 'buddy_clamp': 417 | buddy_img = Image.open( 418 | f"{UTAGE_MAIMAI_MUSIC_DATA}/others/buddy.png" 419 | ).convert('RGBA') 420 | self.base_img.paste( 421 | buddy_img, 422 | (0, 0), 423 | buddy_img.split()[3] 424 | ) 425 | 426 | 427 | def get_font(font_name: str, font_size: int): 428 | """ 429 | Get font. 430 | """ 431 | return ImageFont.truetype( 432 | f"{FONT_PATH}/{font_name}", 433 | font_size, 434 | encoding='utf-8' 435 | ) 436 | -------------------------------------------------------------------------------- /src/libraries/maimai/maimai_music_data/utils.py: -------------------------------------------------------------------------------- 1 | def truncate_text(text, font, max_width): 2 | if font.getsize(text)[0] <= max_width: 3 | return text 4 | else: 5 | for i in range(len(text), 0, -1): 6 | if font.getsize(text[:i] + '...')[0] <= max_width: 7 | return text[:i] + '...' 8 | return '...' 9 | 10 | def decimalPoints(num,count): 11 | num = str(int(round(num * 10**count,8)) / 10**count) 12 | if '.0' == num[-2:]: 13 | num += ('0'* (count-1-(len(num.split('.')[1])))) 14 | if len(num.split('.')[1]) < count: 15 | num += ('0'* (count-(len(num.split('.')[1])))) 16 | return num 17 | -------------------------------------------------------------------------------- /src/libraries/maimai/maimai_play_best50/maimai_play_best50_old_design.py: -------------------------------------------------------------------------------- 1 | # Author: xyb, Diving_Fish 2 | import asyncio,requests 3 | import os,io 4 | import math 5 | from typing import Optional, Dict, List, Tuple 6 | import random 7 | from PIL import Image, ImageDraw, ImageFont 8 | from src.libraries.maimai.maimaidx_music import total_list 9 | from src.libraries.maimai.utils import get_cover_path 10 | import asyncio 11 | from pathlib import Path 12 | import time 13 | import aiohttp 14 | from src.libraries.maimai.maimai_play_best50.maimai_recommend import Recommend 15 | from src.libraries.GLOBAL_CONSTANT import MAIPLATE_FILE_ID_MAP 16 | from src.libraries.maimai.maimai_play_best50.utils import get_best_50_data,get_user_all_records,filter_all_perfect_records,filter_best_35_records,filter_all_records,BestList,ChartInfo 17 | import traceback 18 | from src.libraries.GLOBAL_PATH import OLD_BEST_50_PATH,FRAME_PATH,PLATE_PATH,FONT_PATH,ICON_PATH 19 | 20 | # scoreRank = 'D C B BB BBB A AA AAA S S+ SS SS+ SSS SSS+'.split(' ') 21 | # combo = ' FC FC+ AP AP+'.split(' ') 22 | # diffs = 'Basic Advanced Expert Master Re:Master'.split(' ') 23 | # whatscore = 'd c b bb bbb a aa aaa s s+ ss ss+ sss sss+'.split(' ') 24 | 25 | 26 | 27 | comb = ' fc fcp ap app'.split(' ') 28 | isfs = ' fs fsp fsd fsdp'.split(' ') 29 | level_index = 'BSC ADV EXP MST MST_Re'.split(' ') 30 | rankPic = 'D C B BB BBB A AA AAA S Sp SS SSp SSS SSSp'.split(' ') 31 | 32 | 33 | 34 | class DrawBest(object): 35 | def __init__(self,is_abstract:bool,nickName:str,best35Rating,best15Rating,sdBest:BestList, dxBest:BestList,userConfig,best50_style_index:str): 36 | self.pic_dir = f'{OLD_BEST_50_PATH}/' 37 | self.frame_dir = f"{FRAME_PATH}/" 38 | self.plate_dir = f"{PLATE_PATH}/" 39 | self.icon_dir = f'{ICON_PATH}/' 40 | self.font_dir = f'{FONT_PATH}/' 41 | self.baseImg = Image.new("RGBA", (1920, 1700), (0, 0, 0, 0)) 42 | self.is_abstract = is_abstract 43 | self.best35Rating = best35Rating 44 | self.best15Rating = best15Rating 45 | self.playerRating = best35Rating + best15Rating 46 | self.sdBest = sdBest 47 | self.dxBest = dxBest 48 | self.userConfig = userConfig 49 | self.userName = self._stringQ2B(nickName) 50 | 51 | 52 | 53 | def _Q2B(self, uchar): 54 | """单个字符 全角转半角""" 55 | inside_code = ord(uchar) 56 | if inside_code == 0x3000: 57 | inside_code = 0x0020 58 | else: 59 | inside_code -= 0xfee0 60 | if inside_code < 0x0020 or inside_code > 0x7e: # 转完之后不是半角字符返回原来的字符 61 | return uchar 62 | return chr(inside_code) 63 | 64 | def _stringQ2B(self, ustring): 65 | """把字符串全角转半角""" 66 | return "".join([self._Q2B(uchar) for uchar in ustring]) 67 | 68 | 69 | def _get_dxscore_type(self,dxscorepen): 70 | if dxscorepen <= 90: 71 | return "star-1.png" 72 | elif dxscorepen <= 93: 73 | return "star-2.png" 74 | elif dxscorepen <= 95: 75 | return "star-3.png" 76 | elif dxscorepen <= 97: 77 | return "star-4.png" 78 | else: 79 | return "star-5.png" 80 | 81 | def _getCharWidth(self, o) -> int: 82 | widths = [ 83 | (126, 1), (159, 0), (687, 1), (710, 0), (711, 1), (727, 0), (733, 1), (879, 0), (1154, 1), (1161, 0), 84 | (4347, 1), (4447, 2), (7467, 1), (7521, 0), (8369, 1), (8426, 0), (9000, 1), (9002, 2), (11021, 1), 85 | (12350, 2), (12351, 1), (12438, 2), (12442, 0), (19893, 2), (19967, 1), (55203, 2), (63743, 1), 86 | (64106, 2), (65039, 1), (65059, 0), (65131, 2), (65279, 1), (65376, 2), (65500, 1), (65510, 2), 87 | (120831, 1), (262141, 2), (1114109, 1), 88 | ] 89 | if o == 0xe or o == 0xf: 90 | return 0 91 | for num, wid in widths: 92 | if o <= num: 93 | return wid 94 | return 1 95 | 96 | def _coloumWidth(self, s:str): 97 | res = 0 98 | for ch in s: 99 | res += self._getCharWidth(ord(ch)) 100 | return res 101 | 102 | def _changeColumnWidth(self, s:str, len:int) -> str: 103 | res = 0 104 | sList = [] 105 | for ch in s: 106 | res += self._getCharWidth(ord(ch)) 107 | if res <= len: 108 | sList.append(ch) 109 | return ''.join(sList) 110 | 111 | 112 | def _findRaPic(self) -> str: 113 | num = '10' 114 | if self.playerRating >= 15000: 115 | num = '10' 116 | elif self.playerRating >= 14000: 117 | num = '09' 118 | elif self.playerRating >= 13000: 119 | num = '08' 120 | elif self.playerRating >= 12000: 121 | num = '07' 122 | elif self.playerRating >= 10000: 123 | num = '06' 124 | elif self.playerRating >= 7000: 125 | num = '05' 126 | elif self.playerRating >= 4000: 127 | num = '04' 128 | elif self.playerRating >= 2000: 129 | num = '03' 130 | elif self.playerRating >= 1000: 131 | num = '02' 132 | else: 133 | num = '01' 134 | return f'UI_CMN_DXRating_S_{num}.png' 135 | 136 | def _resizePic(self, img:Image.Image, time:float): 137 | return img.resize((int(img.size[0] * time), int(img.size[1] * time))) 138 | 139 | def _drawRating(self, ratingBaseImg:Image.Image): 140 | COLOUMS_RATING = [105, 122, 140, 158, 175] 141 | theRa = int(self.playerRating) 142 | i = 4 143 | while theRa: 144 | digit = theRa % 10 145 | theRa = theRa // 10 146 | digitImg = Image.open(self.pic_dir + f"Rating/UI_NUM_Drating_{digit}.png").convert('RGBA') 147 | digitImg = self._resizePic(digitImg, 0.6) 148 | ratingBaseImg.paste(digitImg, (COLOUMS_RATING[i], 12), mask=digitImg.split()[3]) 149 | i = i - 1 150 | return ratingBaseImg 151 | 152 | def drawGenerateUserInfo(self): 153 | logoImage = Image.open(self.pic_dir + 'Logo.png').convert('RGBA') 154 | logoImage = logoImage.resize((432,208)) 155 | self.baseImg.paste(logoImage,(-19,0), logoImage.split()[3]) 156 | 157 | # 姓名框 158 | plate_id = str(self.userConfig.get('plate','000011')) 159 | if "custom_plate" in plate_id: 160 | plate_id = plate_id.split('custom_plate')[1] 161 | plateImage = Image.open(self.plate_dir + f"custom/UI_Plate_{plate_id}.png").convert('RGBA') 162 | else: 163 | plateImage = Image.open(self.plate_dir + f"UI_Plate_{plate_id}.png").convert('RGBA') 164 | 165 | plateImage = plateImage.resize((942, 152)) 166 | self.baseImg.paste(plateImage,(390,36),plateImage) 167 | 168 | 169 | # 头像 170 | IconImg = Image.open(self.icon_dir + f"UI_Icon_{self.userConfig.get('icon','000011')}.png").convert('RGBA') 171 | IconImg = IconImg.resize((124, 124)) 172 | self.baseImg.paste(IconImg,(405,49),IconImg.split()[3]) 173 | 174 | # Rating 175 | RankLogo = Image.open(self.pic_dir + f"Rating/{self._findRaPic()}").convert('RGBA') 176 | RankLogo = RankLogo.resize((212, 40)) 177 | RankLogo = self._drawRating(RankLogo) 178 | self.baseImg.paste(RankLogo, (546, 49),RankLogo) 179 | 180 | # 名字 181 | namebackgroundImg = Image.open(self.pic_dir + '姓名背景.png').convert('RGBA') 182 | namebackgroundImg = namebackgroundImg.resize((320, 50)) 183 | DXLogoImg = Image.open(self.pic_dir + 'UI_CMN_Name_DX.png').convert('RGBA') 184 | DXLogoImg = DXLogoImg.resize((36, 26)) 185 | namebackgroundImg.paste(DXLogoImg,(270,8),DXLogoImg) 186 | namePlateDraw = ImageDraw.Draw(namebackgroundImg) 187 | tempFont = ImageFont.truetype(f'{self.font_dir}msyh.ttc', 27, encoding='unic') 188 | namePlateDraw.text((15, 2), ' '.join(list(self.userName)), 'black', tempFont) 189 | self.baseImg.paste(namebackgroundImg, (543, 93),namebackgroundImg) 190 | 191 | # 称号 192 | TitleBgImg = Image.open(self.pic_dir + 'UI_CMN_Shougou_Rainbow.png').convert('RGBA') 193 | TitleBgImg = TitleBgImg.resize((313,29)) 194 | TitleBgImgDraw = ImageDraw.Draw(TitleBgImg) 195 | 196 | tempFont = ImageFont.truetype(f'{self.font_dir}adobe_simhei.otf', 17, encoding='utf-8') 197 | playCountInfo = f'MaiMai DX Rating' 198 | TitleBgImgW, TitleBgImgH = TitleBgImg.size 199 | playCountInfoW, playCountInfoH = TitleBgImgDraw.textsize(playCountInfo, tempFont) 200 | textPos = ((TitleBgImgW - playCountInfoW - tempFont.getoffset(playCountInfo)[0]) / 2, 5) 201 | TitleBgImgDraw.text((textPos[0] - 1, textPos[1]), playCountInfo, 'black', tempFont) 202 | TitleBgImgDraw.text((textPos[0] + 1, textPos[1]), playCountInfo, 'black', tempFont) 203 | TitleBgImgDraw.text((textPos[0], textPos[1] - 1), playCountInfo, 'black', tempFont) 204 | TitleBgImgDraw.text((textPos[0], textPos[1] + 1), playCountInfo, 'black', tempFont) 205 | TitleBgImgDraw.text((textPos[0] - 1, textPos[1] - 1), playCountInfo, 'black', tempFont) 206 | TitleBgImgDraw.text((textPos[0] + 1, textPos[1] - 1), playCountInfo, 'black', tempFont) 207 | TitleBgImgDraw.text((textPos[0] - 1, textPos[1] + 1), playCountInfo, 'black', tempFont) 208 | TitleBgImgDraw.text((textPos[0] + 1, textPos[1] + 1), playCountInfo, 'black', tempFont) 209 | TitleBgImgDraw.text(textPos, playCountInfo, 'white', tempFont) 210 | TitleBgImg = self._resizePic(TitleBgImg, 1.05) 211 | self.baseImg.paste(TitleBgImg,(546,145),TitleBgImg) 212 | 213 | 214 | 215 | 216 | 217 | def drawMusicBox(self,musicChartInfo:ChartInfo): 218 | # 判断使用什么难度的成绩背景 219 | MusicBoxImage = Image.open(self.pic_dir + f'box/XP_UI_MSS_MBase_{level_index[musicChartInfo.diff]}.png').convert('RGBA') 220 | MusicBoxImage = self._resizePic(MusicBoxImage, 0.25) 221 | 222 | CoverPath = get_cover_path(musicChartInfo.idNum,self.is_abstract) 223 | 224 | CoverImage = Image.open(CoverPath).convert('RGBA') 225 | CoverImage = CoverImage.resize((86, 86)) 226 | MusicBoxImage.paste(CoverImage, (16, 14), CoverImage.split()[3]) 227 | 228 | 229 | # DX/标准 230 | MusicTypeLogoPath = self.pic_dir + f'UI_UPE_Infoicon_StandardMode.png' 231 | if musicChartInfo.tp == "DX": 232 | MusicTypeLogoPath = self.pic_dir + f'UI_UPE_Infoicon_DeluxeMode.png' 233 | MusicTypeLogoImg = Image.open(MusicTypeLogoPath).convert('RGBA') 234 | MusicTypeLogoImg = self._resizePic(MusicTypeLogoImg, 0.4) 235 | MusicBoxImage.paste(MusicTypeLogoImg, (14, 100), MusicTypeLogoImg.split()[3]) 236 | 237 | MusicBoxImageDraw = ImageDraw.Draw(MusicBoxImage) 238 | 239 | # Title 240 | font = ImageFont.truetype(self.font_dir + "adobe_simhei.otf", 18, encoding='utf-8') 241 | title = musicChartInfo.title 242 | if self._coloumWidth(title) > 16: 243 | title = self._changeColumnWidth(title, 14) + '...' 244 | MusicBoxImageDraw.text((119, 12), title, 'white', font) 245 | # 歌曲ID 246 | font = ImageFont.truetype(self.font_dir + "adobe_simhei.otf", 14, encoding='utf-8') 247 | song_id = musicChartInfo.idNum 248 | MusicBoxImageDraw.text((120, 31), f'id: {song_id}', 'white', font) 249 | 250 | 251 | # 星星 252 | start_mun = musicChartInfo.dxscore / (sum(total_list.by_id(str(musicChartInfo.idNum)).charts[musicChartInfo.diff]['notes'])*3) *100 253 | start_path = self.pic_dir + 'start/' +self._get_dxscore_type(start_mun) 254 | musicDxStartIcon = Image.open(start_path).convert('RGBA') 255 | musicDxStartIcon = self._resizePic(musicDxStartIcon, 0.9) 256 | MusicBoxImage.paste(musicDxStartIcon, (225, 64), musicDxStartIcon.split()[3]) 257 | 258 | # 成绩图标 259 | rankImg = Image.open(self.pic_dir + f'Rank/UI_TTR_Rank_{rankPic[musicChartInfo.scoreId]}.png').convert('RGBA') 260 | rankImg = self._resizePic(rankImg, 0.35) 261 | MusicBoxImage.paste(rankImg, (220, 32), rankImg.split()[3]) 262 | 263 | # FC/AP图标 264 | if musicChartInfo.comboId: 265 | playBoundIconImg = Image.open(self.pic_dir + f'PlayBounds/{comb[musicChartInfo.comboId]}.png').convert('RGBA') 266 | else: 267 | playBoundIconImg = Image.open(self.pic_dir + f'PlayBounds/fc_dummy.png').convert('RGBA') 268 | playBoundIconImg = self._resizePic(playBoundIconImg, 0.4) 269 | # 横向位置和竖直位置 270 | MusicBoxImage.paste(playBoundIconImg, (210, 86), playBoundIconImg.split()[3]) 271 | 272 | # FSD/FS图标 273 | if musicChartInfo.fs: 274 | playBoundIconImg = Image.open(self.pic_dir + f'PlayBounds/{isfs[musicChartInfo.fs]}.png').convert('RGBA') 275 | else: 276 | playBoundIconImg = Image.open(self.pic_dir + f'PlayBounds/fs_dummy.png').convert('RGBA') 277 | playBoundIconImg = self._resizePic(playBoundIconImg, 0.4) 278 | # 横向位置和竖直位置 279 | MusicBoxImage.paste(playBoundIconImg, (252, 86), playBoundIconImg.split()[3]) 280 | 281 | # Base 完成率 Ra 282 | font = ImageFont.truetype(self.font_dir + "adobe_simhei.otf", 28, encoding='utf-8') 283 | # 完成度 284 | MusicBoxImageDraw.text((118, 47), f"{str(format(musicChartInfo.achievement, '.4f'))[0:-5]}", '#fadf62', font) 285 | 286 | font = ImageFont.truetype(self.font_dir + "adobe_simhei.otf", 16, encoding='utf-8') 287 | 288 | # 小数部分 289 | index = str(musicChartInfo.achievement).split('.')[0] 290 | achievement_f = "0" if len(str(musicChartInfo.achievement).split('.')) != 2 else str(musicChartInfo.achievement).split('.')[1] 291 | if int(index) < 100: 292 | MusicBoxImageDraw.text((150, 57), '. '+achievement_f, '#fadf62', font) 293 | else: 294 | MusicBoxImageDraw.text((167, 57), '. '+achievement_f, '#fadf62', font) 295 | 296 | # 下方 297 | font = ImageFont.truetype(self.font_dir + "adobe_simhei.otf", 11, encoding='utf-8') 298 | 299 | # Base -> Rating 300 | MusicBoxImageDraw.text((125, 85), f"定数\n{format(musicChartInfo.ds, '.1f')}", '#000000', font) 301 | 302 | font = ImageFont.truetype(self.font_dir + "adobe_simhei.otf", 20, encoding='utf-8') 303 | 304 | MusicBoxImageDraw.text((151, 85), f'>', '#000000', font) 305 | MusicBoxImageDraw.text((164, 86), f'{musicChartInfo.ra}', '#000000', font) 306 | 307 | MusicBoxImage = self._resizePic(MusicBoxImage,1.2) 308 | return MusicBoxImage 309 | 310 | 311 | 312 | 313 | def draw(self): 314 | backGroundImg = Image.open(self.pic_dir + "background.png") 315 | self.baseImg.paste(backGroundImg,(0,0),backGroundImg.split()[3]) 316 | 317 | # 生成用户信息 318 | self.drawGenerateUserInfo() 319 | 320 | for index,ci in enumerate(self.sdBest): 321 | i = index // 5 # 需要多少行 322 | j = index % 5 # 一行有几个 323 | MusicBoxImg = self.drawMusicBox(ci) 324 | self.baseImg.paste(MusicBoxImg, (35 + 370 * j, 220 + 140 * i), MusicBoxImg.split()[3]) 325 | 326 | 327 | for index,ci in enumerate(self.dxBest): 328 | i = index // 5 # 需要多少行 329 | j = index % 5 # 一行有几个 330 | MusicBoxImg = self.drawMusicBox(ci) 331 | self.baseImg.paste(MusicBoxImg,(35 + 370 * j, 1240 + 140 * i),MusicBoxImg.split()[3]) 332 | return self.baseImg 333 | 334 | 335 | 336 | 337 | def computeRa(ds: float, achievement:float) -> int: 338 | if achievement >= 50 and achievement < 60: 339 | baseRa = 6.0 340 | elif achievement < 70: 341 | baseRa = 7.0 342 | elif achievement < 75: 343 | baseRa = 7.5 344 | elif achievement < 80: 345 | baseRa = 8.5 346 | elif achievement < 90: 347 | baseRa = 9.5 348 | elif achievement < 94: 349 | baseRa = 10.5 350 | elif achievement < 97: 351 | baseRa = 12.5 352 | elif achievement < 98: 353 | baseRa = 12.7 354 | elif achievement < 99: 355 | baseRa = 13.0 356 | elif achievement < 99.5: 357 | baseRa = 13.2 358 | elif achievement < 100: 359 | baseRa = 13.5 360 | elif achievement < 100.5: 361 | baseRa = 14.0 362 | else: 363 | baseRa = 5.0 364 | 365 | return math.floor(ds * (min(100.5, achievement) / 100) * baseRa) 366 | 367 | async def generate_ap50(payload: Dict,is_abstract:bool,userConfig,best50_style:str): 368 | payload = payload 369 | status,request_json = await get_user_all_records(payload) 370 | if status == 400: 371 | return None, 400, 0 372 | records = request_json['records'] 373 | b35_ap_records,b15_ap_records = filter_all_perfect_records(records) 374 | b35_ap_records = b35_ap_records[:35] 375 | b15_ap_records = b15_ap_records[:15] 376 | sd_best = BestList(35) 377 | dx_best = BestList(15) 378 | for c in b35_ap_records: 379 | sd_best.push(ChartInfo.from_json(c)) 380 | for c in b15_ap_records: 381 | dx_best.push(ChartInfo.from_json(c)) 382 | nickName = request_json['nickname'] 383 | if b35_ap_records or b15_ap_records: 384 | best35Rating = sum([item['ra'] for item in b35_ap_records]) 385 | best15Rating = sum([item['ra'] for item in b15_ap_records]) 386 | rating = best35Rating+best15Rating 387 | plate_id = userConfig.get('plate',"") 388 | if plate_id == "": 389 | df_plate = request_json['plate'] 390 | if df_plate: 391 | userConfig['plate'] = MAIPLATE_FILE_ID_MAP.get(df_plate,"000011") 392 | else: 393 | userConfig['plate'] = "000011" 394 | pic = DrawBest(is_abstract,nickName,best15Rating,best35Rating,sd_best, dx_best,userConfig,best50_style).draw() 395 | return pic, 0, rating 396 | else: 397 | return None, 0, 0 398 | 399 | async def generate_best_35(payload: Dict,is_abstract:bool,userConfig,best50_style:str): 400 | payload = payload 401 | status,request_json = await get_user_all_records(payload) 402 | if status == 400: 403 | return None, 400, 0 404 | records = request_json['records'] 405 | b35_ap_records,b15_ap_records = filter_best_35_records(records) 406 | b35_ap_records = b35_ap_records[:35] 407 | b15_ap_records = b15_ap_records[:15] 408 | sd_best = BestList(35) 409 | dx_best = BestList(15) 410 | for c in b35_ap_records: 411 | sd_best.push(ChartInfo.from_json(c)) 412 | for c in b15_ap_records: 413 | dx_best.push(ChartInfo.from_json(c)) 414 | nickName = request_json['nickname'] 415 | if b35_ap_records or b15_ap_records: 416 | best35Rating = sum([item['ra'] for item in b35_ap_records]) 417 | best15Rating = sum([item['ra'] for item in b15_ap_records]) 418 | rating = best35Rating+best15Rating 419 | plate_id = userConfig.get('plate',"") 420 | if plate_id == "": 421 | df_plate = request_json['plate'] 422 | if df_plate: 423 | userConfig['plate'] = MAIPLATE_FILE_ID_MAP.get(df_plate,"000011") 424 | else: 425 | userConfig['plate'] = "000011" 426 | pic = DrawBest(is_abstract,nickName,best15Rating,best35Rating,sd_best, dx_best,userConfig,best50_style).draw() 427 | return pic, 0, rating 428 | else: 429 | return None, 0, 0 430 | 431 | async def generate_best_50(payload: Dict,is_abstract:bool,userConfig,best50_style:str,mode:str): 432 | status,request_json,best35_records,best15_records,best35_BestList,best15_BestList = await get_best_50_data(payload,mode) 433 | if status == 400: 434 | return None, 400, 0 435 | if status == 403: 436 | return None, 403, 0 437 | if best35_records or best15_records: 438 | best35Rating = sum([item['ra'] for item in best35_records]) 439 | best15Rating = sum([item['ra'] for item in best15_records]) 440 | rating = best35Rating+best15Rating 441 | nickName = request_json['nickname'] 442 | plate_id = userConfig.get('plate',"") 443 | if plate_id == "": 444 | df_plate = request_json['plate'] 445 | if df_plate: 446 | userConfig['plate'] = MAIPLATE_FILE_ID_MAP.get(df_plate,"000011") 447 | else: 448 | userConfig['plate'] = "000011" 449 | pic = DrawBest(is_abstract,nickName,best15Rating,best35Rating,best35_BestList, best15_BestList,userConfig,best50_style).draw() 450 | 451 | return pic, 0, rating 452 | else: 453 | return None, 0, 0 454 | 455 | async def generate_b50(payload: Dict,is_abstract:bool,userConfig,best50_style:str): 456 | async with aiohttp.request("POST", "https://www.diving-fish.com/api/maimaidxprober/query/player", json=payload) as resp: 457 | if resp.status == 400: 458 | return None, 400, 0 459 | if resp.status == 403: 460 | return None, 403, 0 461 | sd_best = BestList(35) 462 | dx_best = BestList(15) 463 | obj = await resp.json() 464 | dx: List[Dict] = obj["charts"]["dx"] 465 | sd: List[Dict] = obj["charts"]["sd"] 466 | rating = int(str(obj["rating"])) 467 | for c in sd: 468 | sd_best.push(ChartInfo.from_json(c)) 469 | for c in dx: 470 | dx_best.push(ChartInfo.from_json(c)) 471 | nickName = obj['nickname'] 472 | if rating > 0: 473 | best35Rating = sum([item['ra'] for item in obj["charts"]["sd"]]) 474 | best15Rating = sum([item['ra'] for item in obj["charts"]["dx"]]) 475 | plate_id = userConfig.get('plate',"") 476 | if plate_id == "": 477 | df_plate = obj['plate'] 478 | if df_plate: 479 | userConfig['plate'] = MAIPLATE_FILE_ID_MAP.get(df_plate,"000011") 480 | else: 481 | userConfig['plate'] = "000011" 482 | 483 | pic = DrawBest(is_abstract,nickName,best15Rating,best35Rating,sd_best, dx_best,userConfig,best50_style).draw() 484 | return pic, 0, rating 485 | else: 486 | return None, 0, 0 487 | 488 | async def generate_all_best50(payload: Dict,is_abstract:bool,userConfig,best50_style:str): 489 | payload = payload 490 | status,request_json = await get_user_all_records(payload) 491 | if status == 400: 492 | return None, 400, 0 493 | records = request_json['records'] 494 | b35_ap_records,b15_ap_records = filter_all_records(records) 495 | b35_ap_records = b35_ap_records[:50] 496 | b15_ap_records = b15_ap_records[:15] 497 | sd_best = BestList(50) 498 | dx_best = BestList(15) 499 | for c in b35_ap_records: 500 | sd_best.push(ChartInfo.from_json(c)) 501 | for c in b15_ap_records: 502 | dx_best.push(ChartInfo.from_json(c)) 503 | nickName = request_json['nickname'] 504 | if b35_ap_records or b15_ap_records: 505 | best35Rating = sum([item['ra'] for item in b35_ap_records]) 506 | best15Rating = sum([item['ra'] for item in b15_ap_records]) 507 | rating = best35Rating+best15Rating 508 | plate_id = userConfig.get('plate',"") 509 | if plate_id == "": 510 | df_plate = request_json['plate'] 511 | if df_plate: 512 | userConfig['plate'] = MAIPLATE_FILE_ID_MAP.get(df_plate,"000011") 513 | else: 514 | userConfig['plate'] = "000011" 515 | pic = DrawBest(is_abstract,nickName,best15Rating,best35Rating,sd_best, dx_best,userConfig,best50_style).draw() 516 | return pic, 0, rating 517 | else: 518 | return None, 0, 0 -------------------------------------------------------------------------------- /src/libraries/maimai/maimai_play_best50/maimai_recommend.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List 2 | 3 | class Recommend: 4 | def __init__(self,musicRating): 5 | self.recommendData = {} 6 | self.recommendData["Rating"] = musicRating 7 | self.recommendData["RankSSSP"] = self.get_TargetDs(musicRating, 100.5) 8 | self.recommendData["RankSSS"] = self.get_TargetDs(musicRating, 100.0) 9 | self.recommendData["RankSSP"] = self.get_TargetDs(musicRating, 99.5) 10 | self.recommendData["RankSS"] = self.get_TargetDs(musicRating, 99.0) 11 | 12 | 13 | def getRatingRecommendData(self): 14 | return self.recommendData 15 | 16 | def computeRa(self,ds: float, achievement: float) -> int: 17 | baseRa = 22.4 18 | if achievement < 50: 19 | baseRa = 7.0 20 | elif achievement < 60: 21 | baseRa = 8.0 22 | elif achievement < 70: 23 | baseRa = 9.6 24 | elif achievement < 75: 25 | baseRa = 11.2 26 | elif achievement < 80: 27 | baseRa = 12.0 28 | elif achievement < 90: 29 | baseRa = 13.6 30 | elif achievement < 94: 31 | baseRa = 15.2 32 | elif achievement < 97: 33 | baseRa = 16.8 34 | elif achievement < 98: 35 | baseRa = 20.0 36 | elif achievement < 99: 37 | baseRa = 20.3 38 | elif achievement < 99.5: 39 | baseRa = 20.8 40 | elif achievement < 100: 41 | baseRa = 21.1 42 | elif achievement < 100.5: 43 | baseRa = 21.6 44 | 45 | return int(ds * (min(100.5, achievement) / 100) * baseRa) 46 | 47 | def get_TargetDs(self, ra: int, rank: float): 48 | for d in [x/10 for x in range(10,151)]: 49 | if self.computeRa(d, rank) in [ra, ra+1,ra+2]: 50 | return d 51 | return -1 -------------------------------------------------------------------------------- /src/libraries/maimai/maimai_play_best50/utils.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | import nonebot 3 | from src.libraries.maimai.maimaidx_music import total_list 4 | from typing import Dict, List 5 | import re 6 | from src.libraries.GLOBAL_CONSTANT import NOW_VERSION,DEVELOPER_TOKEN 7 | 8 | scoreRank = 'D C B BB BBB A AA AAA S S+ SS SS+ SSS SSS+'.split(' ') 9 | combo = ' FC FC+ AP AP+'.split(' ') 10 | diffs = 'Basic Advanced Expert Master Re:Master'.split(' ') 11 | comb = ' fc fcp ap app'.split(' ') 12 | isfs = ' fs fsp fsd fsdp'.split(' ') 13 | whatscore = 'd c b bb bbb a aa aaa s s+ ss ss+ sss sss+'.split(' ') 14 | 15 | 16 | class ChartInfo(object): 17 | def __init__(self, idNum: str, diff: int, tp: str, achievement: float, ra: int, comboId: int, scoreId: int, 18 | title: str, ds: float, lv: str, fs: int, dxscore: int): 19 | self.idNum = idNum 20 | self.diff = diff 21 | self.tp = tp 22 | self.achievement = achievement 23 | self.dxscore = dxscore 24 | if achievement >= 100.5: 25 | self.ra = int(ds * 22.4 * 100.5 / 100) 26 | elif achievement >= 100: 27 | self.ra = int(ds * 21.6 * achievement / 100) 28 | elif achievement >= 99.5: 29 | self.ra = int(ds * 21.1 * achievement / 100) 30 | elif achievement >= 99: 31 | self.ra = int(ds * 20.8 * achievement / 100) 32 | elif achievement >= 98: 33 | self.ra = int(ds * 20.3 * achievement / 100) 34 | elif achievement >= 97: 35 | self.ra = int(ds * 20 * achievement / 100) 36 | elif achievement >= 94: 37 | self.ra = int(ds * 16.8 * achievement / 100) 38 | elif achievement >= 90: 39 | self.ra = int(ds * 15.2 * achievement / 100) 40 | elif achievement >= 80: 41 | self.ra = int(ds * 13.6 * achievement / 100) 42 | elif achievement >= 75: 43 | self.ra = int(ds * 12.0 * achievement / 100) 44 | elif achievement >= 70: 45 | self.ra = int(ds * 11.2 * achievement / 100) 46 | elif achievement >= 60: 47 | self.ra = int(ds * 9.6 * achievement / 100) 48 | elif achievement >= 50: 49 | self.ra = int(ds * 8 * achievement / 100) 50 | else: 51 | self.ra = 0 52 | self.comboId = comboId 53 | self.scoreId = scoreId 54 | self.title = title 55 | self.ds = ds 56 | self.lv = lv 57 | self.fs = fs 58 | 59 | def __str__(self): 60 | return '%-50s' % f'{self.title} [{self.tp}]' + f'{self.ds}\t{diffs[self.diff]}\t{self.ra}' 61 | 62 | def __eq__(self, other): 63 | return self.ra == other.ra 64 | 65 | def __lt__(self, other): 66 | return self.ra < other.ra 67 | 68 | @classmethod 69 | def sp_from_json(cls, data): 70 | rate = ['d', 'c', 'b', 'bb', 'bbb', 'a', 'aa', 71 | 'aaa', 's', 'sp', 'ss', 'ssp', 'sss', 'sssp'] 72 | ri = rate.index(data["rate"]) 73 | fc = ['', 'fc', 'fcp', 'ap', 'app'] 74 | fi = fc.index(data["fc"]) 75 | fs = ['', 'fs', 'fsp', 'fsd', 'fsdp'] 76 | fsi = fs.index(data['fs']) 77 | # idNum=total_list.by_title(data["title"]).id, 78 | 79 | return cls( 80 | idNum=data['song_id'], 81 | title=data["title"], 82 | diff=data["level_index"], 83 | ra=data["ra"], 84 | ds=data["ds"], 85 | comboId=fi, 86 | scoreId=ri, 87 | lv=data["level"], 88 | achievement=data["achievements"], 89 | tp=data["type"], 90 | fs=fsi, 91 | dxscore=data["dxScore"] 92 | ) 93 | 94 | @classmethod 95 | def from_json(cls, data): 96 | rate = ['d', 'c', 'b', 'bb', 'bbb', 'a', 'aa', 97 | 'aaa', 's', 'sp', 'ss', 'ssp', 'sss', 'sssp'] 98 | ri = rate.index(data["rate"]) 99 | fc = ['', 'fc', 'fcp', 'ap', 'app'] 100 | fi = fc.index(data["fc"]) 101 | fs = ['', 'fs', 'fsp', 'fsd', 'fsdp'] 102 | try: 103 | fsi = fs.index(data['fs']) 104 | except: 105 | fsi = 0 106 | # idNum=total_list.by_title(data["title"]).id, 107 | 108 | return cls( 109 | idNum=str(data['song_id']), 110 | title=data["title"], 111 | diff=data["level_index"], 112 | ra=data["ra"], 113 | ds=data["ds"], 114 | comboId=fi, 115 | scoreId=ri, 116 | lv=data["level"], 117 | achievement=data["achievements"], 118 | tp=data["type"], 119 | fs=fsi, 120 | dxscore=data["dxScore"] 121 | ) 122 | 123 | 124 | class BestList(object): 125 | 126 | def __init__(self, size: int): 127 | self.data = [] 128 | self.size = size 129 | 130 | def push(self, elem: ChartInfo): 131 | if len(self.data) >= self.size and elem < self.data[-1]: 132 | return 133 | self.data.append(elem) 134 | self.data.sort() 135 | self.data.reverse() 136 | while (len(self.data) > self.size): 137 | del self.data[-1] 138 | 139 | def pop(self): 140 | del self.data[-1] 141 | 142 | def __str__(self): 143 | return '[\n\t' + ', \n\t'.join([str(ci) for ci in self.data]) + '\n]' 144 | 145 | def __len__(self): 146 | return len(self.data) 147 | 148 | def __getitem__(self, index): 149 | return self.data[index] 150 | 151 | 152 | def filter_all_perfect_records(records): 153 | b15_all_perfect_records = [] 154 | b35_all_perfect_records = [] 155 | for item in records: 156 | if item['fc'] in ['ap', 'app']: 157 | if total_list.by_id(str(item['song_id'])).cn_version == NOW_VERSION: 158 | b15_all_perfect_records.append(item) 159 | else: 160 | b35_all_perfect_records.append(item) 161 | 162 | b35_sorted_data = sorted(b35_all_perfect_records, 163 | key=lambda x: x['ra'], reverse=True) 164 | b15_sorted_data = sorted(b15_all_perfect_records, 165 | key=lambda x: x['ra'], reverse=True) 166 | return b35_sorted_data[:35], b15_sorted_data[:15] 167 | 168 | 169 | def filter_full_combo_records(records): 170 | b15_full_combo_records = [] 171 | b35_full_combo_records = [] 172 | for item in records: 173 | if item['fc'] in ['fc', 'fcp','ap', 'app']: 174 | if total_list.by_id(str(item['song_id'])).cn_version == NOW_VERSION: 175 | b15_full_combo_records.append(item) 176 | else: 177 | b35_full_combo_records.append(item) 178 | 179 | b35_sorted_data = sorted(b35_full_combo_records, 180 | key=lambda x: x['ra'], reverse=True) 181 | b15_sorted_data = sorted(b15_full_combo_records, 182 | key=lambda x: x['ra'], reverse=True) 183 | return b35_sorted_data[:35], b15_sorted_data[:15] 184 | 185 | 186 | def filter_full_combo_plus_records(records): 187 | b15_full_combo_records = [] 188 | b35_full_combo_records = [] 189 | for item in records: 190 | if item['fc'] in ['fcp','ap', 'app']: 191 | if total_list.by_id(str(item['song_id'])).cn_version == NOW_VERSION: 192 | b15_full_combo_records.append(item) 193 | else: 194 | b35_full_combo_records.append(item) 195 | 196 | b35_sorted_data = sorted(b35_full_combo_records, 197 | key=lambda x: x['ra'], reverse=True) 198 | b15_sorted_data = sorted(b15_full_combo_records, 199 | key=lambda x: x['ra'], reverse=True) 200 | return b35_sorted_data[:35], b15_sorted_data[:15] 201 | 202 | 203 | 204 | def filter_best_35_records(records): 205 | b15_all_perfect_records = [] 206 | b35_all_perfect_records = [] 207 | for item in records: 208 | achievement = item['achievements'] 209 | ds = total_list.by_id(str(item['song_id'])).ds[item['level_index']] 210 | if achievement >= 100.5: 211 | ra = int(ds * 22.4 * 100.5 / 100) 212 | elif achievement >= 100: 213 | ra = int(ds * 21.6 * achievement / 100) 214 | elif achievement >= 99.5: 215 | ra = int(ds * 21.1 * achievement / 100) 216 | elif achievement >= 99: 217 | ra = int(ds * 20.8 * achievement / 100) 218 | elif achievement >= 98: 219 | ra = int(ds * 20.3 * achievement / 100) 220 | elif achievement >= 97: 221 | ra = int(ds * 20 * achievement / 100) 222 | elif achievement >= 94: 223 | ra = int(ds * 16.8 * achievement / 100) 224 | elif achievement >= 90: 225 | ra = int(ds * 15.2 * achievement / 100) 226 | elif achievement >= 80: 227 | ra = int(ds * 13.6 * achievement / 100) 228 | elif achievement >= 75: 229 | ra = int(ds * 12.0 * achievement / 100) 230 | elif achievement >= 70: 231 | ra = int(ds * 11.2 * achievement / 100) 232 | elif achievement >= 60: 233 | ra = int(ds * 9.6 * achievement / 100) 234 | elif achievement >= 50: 235 | ra = int(ds * 8 * achievement / 100) 236 | else: 237 | ra = 0 238 | item['ra'] = ra 239 | item['ds'] = ds 240 | b35_all_perfect_records.append(item) 241 | 242 | b35_sorted_data = sorted(b35_all_perfect_records, 243 | key=lambda x: x['ra'], reverse=True) 244 | b15_sorted_data = sorted(b15_all_perfect_records, 245 | key=lambda x: x['ra'], reverse=True) 246 | return b35_sorted_data[:35], b15_sorted_data[:15] 247 | 248 | 249 | def filter_all_records(records): 250 | all_records = [] 251 | for item in records: 252 | all_records.append(item) 253 | 254 | all_sorted_data = sorted(all_records, key=lambda x: x['ra'], reverse=True) 255 | return all_sorted_data[:50], [] 256 | 257 | def filter_level_best_35_records(records,_level): 258 | b15_records = [] 259 | b35_records = [] 260 | for item in records: 261 | music_info = total_list.by_id(str(item['song_id'])) 262 | if music_info.level[item['level_index']]==_level: 263 | if music_info.cn_version == NOW_VERSION: 264 | b15_records.append(item) 265 | else: 266 | b35_records.append(item) 267 | 268 | b35_sorted_data = sorted(b35_records, 269 | key=lambda x: x['ra'], reverse=True) 270 | b15_sorted_data = sorted(b15_records, 271 | key=lambda x: x['ra'], reverse=True) 272 | return b35_sorted_data[:35], b15_sorted_data[:15] 273 | 274 | 275 | def filter_c_best_35_records(records): 276 | b15_records = [] 277 | b35_records = [] 278 | for item in records: 279 | achievement = item['achievements'] 280 | 281 | if 96.9800 < achievement < 97 or 98.9800 < achievement < 99 or 99.4800 < achievement < 99.5 or 99.9800 < achievement < 100 or 100.4800 < achievement < 100.5: 282 | music_info = total_list.by_id(str(item['song_id'])) 283 | if music_info.cn_version == NOW_VERSION: 284 | b15_records.append(item) 285 | else: 286 | b35_records.append(item) 287 | 288 | b35_sorted_data = sorted(b35_records, 289 | key=lambda x: x['ra'], reverse=True) 290 | b15_sorted_data = sorted(b15_records, 291 | key=lambda x: x['ra'], reverse=True) 292 | return b35_sorted_data[:35], b15_sorted_data[:15] 293 | 294 | def filter_m_best_35_records(records): 295 | b15_records = [] 296 | b35_records = [] 297 | for item in records: 298 | achievement = item['achievements'] 299 | 300 | if 100 <= achievement <= 100.0100 or 100.5 <= achievement <= 100.5100: 301 | music_info = total_list.by_id(str(item['song_id'])) 302 | if music_info.cn_version == NOW_VERSION: 303 | b15_records.append(item) 304 | else: 305 | b35_records.append(item) 306 | 307 | b35_sorted_data = sorted(b35_records, 308 | key=lambda x: x['ra'], reverse=True) 309 | b15_sorted_data = sorted(b15_records, 310 | key=lambda x: x['ra'], reverse=True) 311 | return b35_sorted_data[:35], b15_sorted_data[:15] 312 | 313 | async def get_user_all_records(payload): 314 | headers = { 315 | "developer-token": DEVELOPER_TOKEN 316 | } 317 | async with aiohttp.request("GET", "https://www.diving-fish.com/api/maimaidxprober/dev/player/records", params=payload, headers=headers) as resp: 318 | request_json = await resp.json() 319 | return resp.status, request_json 320 | 321 | filter_map = { 322 | "all":filter_all_records, 323 | "a":filter_all_records, 324 | "2024":filter_best_35_records, 325 | "ap":filter_all_perfect_records, 326 | "fc":filter_full_combo_records, 327 | "fc+":filter_full_combo_plus_records, 328 | "寸":filter_c_best_35_records, 329 | "c":filter_c_best_35_records, 330 | "名刀":filter_m_best_35_records 331 | } 332 | 333 | async def get_best_50_data(payload, filter_mode): 334 | if filter_mode is None: 335 | async with aiohttp.request("POST", "https://www.diving-fish.com/api/maimaidxprober/query/player", json=payload) as resp: 336 | obj = await resp.json() 337 | if resp.status == 400: 338 | return resp.status, obj, None, None, None, None 339 | if resp.status == 403: 340 | return resp.status, obj, None, None, None, None 341 | sd_best = BestList(35) 342 | dx_best = BestList(15) 343 | dx: List[Dict] = obj["charts"]["dx"] 344 | sd: List[Dict] = obj["charts"]["sd"] 345 | for c in sd: 346 | sd_best.push(ChartInfo.from_json(c)) 347 | for c in dx: 348 | dx_best.push(ChartInfo.from_json(c)) 349 | return resp.status, obj, sd, dx, sd_best, dx_best 350 | else: 351 | status,request_json = await get_user_all_records(payload) 352 | if status == 400: 353 | return status,request_json, None, None, None, None 354 | records = request_json['records'] 355 | 356 | regex = r'(\d+)(\+)?' 357 | match = re.match(regex, filter_mode) 358 | if match: 359 | level,is_plus = match.groups() 360 | if 1 <= int(level) <= 15: 361 | if is_plus =="+": 362 | level = level+"+" 363 | best35_record,best15_records = filter_level_best_35_records(records,level) 364 | else: 365 | filter_fun = filter_map[filter_mode] 366 | best35_record,best15_records = filter_fun(records) 367 | else: 368 | filter_fun = filter_map[filter_mode] 369 | best35_record,best15_records = filter_fun(records) 370 | sd_best = BestList(50) 371 | dx_best = BestList(50) 372 | for c in best35_record: 373 | sd_best.push(ChartInfo.from_json(c)) 374 | for c in best15_records: 375 | dx_best.push(ChartInfo.from_json(c)) 376 | return status,request_json, best35_record, best15_records, sd_best, dx_best -------------------------------------------------------------------------------- /src/libraries/maimai/maimaidx_music.py: -------------------------------------------------------------------------------- 1 | import json 2 | import random 3 | from typing import Dict, List, Optional, Union, Tuple, Any 4 | from copy import deepcopy 5 | from src.libraries.GLOBAL_CONSTANT import VERSION_EZ_MAP,DELETED_MUSIC 6 | from src.libraries.GLOBAL_PATH import DATA_MAIMAI_PATH,CHN_MUSIC_DATA_URL 7 | from src.libraries.data_handle.abstract_db_handle import abstract 8 | import requests 9 | 10 | 11 | def cross(checker: List[Any], elem: Optional[Union[Any, List[Any]]], diff): 12 | ret = False 13 | diff_ret = [] 14 | if not elem or elem is Ellipsis: 15 | return True, diff 16 | if isinstance(elem, List): 17 | for _j in (range(len(checker)) if diff is Ellipsis else diff): 18 | if _j >= len(checker): 19 | continue 20 | __e = checker[_j] 21 | if __e in elem: 22 | diff_ret.append(_j) 23 | ret = True 24 | elif isinstance(elem, Tuple): 25 | for _j in (range(len(checker)) if diff is Ellipsis else diff): 26 | if _j >= len(checker): 27 | continue 28 | __e = checker[_j] 29 | if elem[0] <= __e <= elem[1]: 30 | diff_ret.append(_j) 31 | ret = True 32 | else: 33 | for _j in (range(len(checker)) if diff is Ellipsis else diff): 34 | if _j >= len(checker): 35 | continue 36 | __e = checker[_j] 37 | if elem == __e: 38 | return True, [_j] 39 | return ret, diff_ret 40 | 41 | 42 | def in_or_equal(checker: Any, elem: Optional[Union[Any, List[Any]]]): 43 | if elem is Ellipsis: 44 | return True 45 | if isinstance(elem, List): 46 | return checker in elem 47 | elif isinstance(elem, Tuple): 48 | return elem[0] <= checker <= elem[1] 49 | else: 50 | return checker == elem 51 | 52 | 53 | class Chart(Dict): 54 | tap: Optional[int] = None 55 | slide: Optional[int] = None 56 | hold: Optional[int] = None 57 | touch: Optional[int] = None 58 | brk: Optional[int] = None 59 | charter: Optional[int] = None 60 | 61 | def __getattribute__(self, item): 62 | if item == 'tap': 63 | return self['notes'][0] 64 | elif item == 'hold': 65 | return self['notes'][1] 66 | elif item == 'slide': 67 | return self['notes'][2] 68 | elif item == 'touch': 69 | return self['notes'][3] if len(self['notes']) == 5 else 0 70 | elif item == 'brk': 71 | return self['notes'][-1] 72 | elif item == 'charter': 73 | return self['charter'] 74 | return super().__getattribute__(item) 75 | 76 | 77 | class Music(Dict): 78 | id: Optional[str] = None 79 | title: Optional[str] = None 80 | ds: Optional[List[float]] = None 81 | level: Optional[List[str]] = None 82 | genre: Optional[str] = None 83 | type: Optional[str] = None 84 | bpm: Optional[float] = None 85 | version: Optional[str] = None 86 | charts: Optional[Chart] = None 87 | release_date: Optional[str] = None 88 | artist: Optional[str] = None 89 | cn_version : Optional[str] = None 90 | ez_version : Optional[str] = None 91 | 92 | diff: List[int] = [] 93 | 94 | def __getattribute__(self, item): 95 | if item in {'genre', 'artist', 'release_date', 'bpm', 'version','cn_version','ez_version'}: 96 | if item == 'version': 97 | return self['basic_info']['from'] 98 | if item == 'cn_version': 99 | return self['basic_info']['cn_from'] 100 | if item == 'ez_version': 101 | return self['basic_info']['ez_from'] 102 | return self['basic_info'][item] 103 | elif item in self: 104 | return self[item] 105 | return super().__getattribute__(item) 106 | 107 | 108 | 109 | class MusicList(List[Music]): 110 | def by_version_for_plate(self,music_version) -> Optional[Music]: 111 | musiclist = [] 112 | for music in self: 113 | if int(music.id) in DELETED_MUSIC: 114 | continue 115 | if music.version in music_version: 116 | musiclist.append(music) 117 | return musiclist 118 | 119 | def by_versions_for_cn(self,music_version) -> Optional[Music]: 120 | musiclist = [] 121 | for music in self: 122 | if int(music.id) in DELETED_MUSIC: 123 | continue 124 | if music.cn_version in music_version: 125 | musiclist.append(music) 126 | return musiclist 127 | 128 | def by_id(self, music_id: str) -> Optional[Music]: 129 | for music in self: 130 | if music.id == music_id: 131 | return music 132 | return None 133 | 134 | def by_title(self, music_title: str) -> Optional[Music]: 135 | for music in self: 136 | if music.title == music_title: 137 | return music 138 | return None 139 | 140 | def by_version(self,music_version: str) -> Optional[Music]: 141 | 142 | for music in self: 143 | # print(music.version) 144 | if music.version == music_version: 145 | return music 146 | return None 147 | 148 | 149 | def get_version_music(self,music_version: str): 150 | new_list = MusicList() 151 | for music in self: 152 | # print(music_version,music_version) 153 | if music.version == music_version: 154 | new_list.append(music) 155 | return new_list 156 | 157 | def get_othversion_music(self,music_version: str): 158 | new_list = MusicList() 159 | for music in self: 160 | # print(music_version,music_version) 161 | if music.version != music_version: 162 | new_list.append(music) 163 | return new_list 164 | 165 | def random(self): 166 | return random.choice(self) 167 | 168 | def random_no_eng(self): 169 | while True: 170 | music = random.choice(self) 171 | need_continue = False 172 | for c in music.title: 173 | if c in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ": 174 | need_continue = True 175 | break 176 | if need_continue:continue 177 | break 178 | return music 179 | 180 | def level_unfinish_filter(self,level): 181 | new_list = MusicList() 182 | for music in self: 183 | # print(music.level) 184 | if level in music.level: 185 | new_list.append(music) 186 | return new_list 187 | 188 | def filter(self, 189 | *, 190 | level: Optional[Union[str, List[str]]] = ..., 191 | ds: Optional[Union[float, List[float], Tuple[float, float]]] = ..., 192 | title_search: Optional[str] = ..., 193 | genre: Optional[Union[str, List[str]]] = ..., 194 | bpm: Optional[Union[float, List[float], Tuple[float, float]]] = ..., 195 | type: Optional[Union[str, List[str]]] = ..., 196 | diff: List[int] = ..., 197 | ): 198 | new_list = MusicList() 199 | for music in self: 200 | diff2 = diff 201 | music = deepcopy(music) 202 | ret, diff2 = cross(music.level, level, diff2) 203 | if not ret: 204 | continue 205 | ret, diff2 = cross(music.ds, ds, diff2) 206 | if not ret: 207 | continue 208 | if not in_or_equal(music.genre, genre): 209 | continue 210 | if not in_or_equal(music.type, type): 211 | continue 212 | if not in_or_equal(music.bpm, bpm): 213 | continue 214 | if title_search is not Ellipsis and title_search.lower() not in music.title.lower(): 215 | continue 216 | # if not music.version == ver: 217 | # continue 218 | music.diff = diff2 219 | new_list.append(music) 220 | return new_list 221 | 222 | 223 | 224 | obj = requests.get(CHN_MUSIC_DATA_URL).json() 225 | 226 | for item in obj: 227 | if item['basic_info']['from'] == "maimai でらっくす": 228 | item['basic_info']['cn_from'] = "舞萌DX" 229 | elif item['basic_info']['from'] == "maimai でらっくす Splash": 230 | item['basic_info']['cn_from'] = "舞萌DX 2021" 231 | elif item['basic_info']['from'] == "maimai でらっくす UNiVERSE": 232 | item['basic_info']['cn_from'] = "舞萌DX 2022" 233 | elif item['basic_info']['from'] == "maimai でらっくす FESTiVAL": 234 | item['basic_info']['cn_from'] = "舞萌DX 2023" 235 | elif item['basic_info']['from'] == "maimai でらっくす BUDDiES": 236 | item['basic_info']['cn_from'] = "舞萌DX 2024" 237 | else: 238 | item['basic_info']['cn_from'] = item['basic_info']['from'] 239 | 240 | if item['basic_info']['cn_from'] == 'maimai': 241 | item['basic_info']['ez_from'] = 'maimai' 242 | else: 243 | ez_ver = str(item['basic_info']['cn_from']).replace("maimai ","") 244 | item['basic_info']['ez_from'] = f"{ez_ver}({VERSION_EZ_MAP.get(ez_ver,ez_ver)})" 245 | 246 | 247 | 248 | if item['basic_info']['genre'] in ['舞萌','maimai']: 249 | item['basic_info']['genre'] = '舞萌痴' 250 | if item['basic_info']['genre'] in ['POPSアニメ','流行&动漫']: 251 | item['basic_info']['genre'] = '现充&二次元' 252 | if item['basic_info']['genre'] in ['音击&中二节奏','オンゲキCHUNITHM']: 253 | item['basic_info']['genre'] = '幼击&除你祖母' 254 | if item['basic_info']['genre'] in ['niconico & VOCALOID','niconicoボーカロイド']: 255 | item['basic_info']['genre'] = '术&力&口' 256 | if item['basic_info']['genre'] in ['東方Project','东方Project']: 257 | item['basic_info']['genre'] = '车万Project' 258 | if item['basic_info']['genre'] in ['其他游戏','ゲームバラエティ']: 259 | item['basic_info']['genre'] = '骑她游嘻' 260 | 261 | 262 | 263 | 264 | total_list: MusicList = MusicList(obj) 265 | 266 | for __i in range(len(total_list)): 267 | total_list[__i] = Music(total_list[__i]) 268 | for __j in range(len(total_list[__i].charts)): 269 | total_list[__i].charts[__j] = Chart(total_list[__i].charts[__j]) 270 | 271 | 272 | demolist = abstract.get_abstract_id_list() 273 | unfinishobj = [] 274 | for item in obj: 275 | try: 276 | if int(item['id']) not in demolist: 277 | unfinishobj.append(item) 278 | except: 279 | pass 280 | 281 | unfinish_list: MusicList = MusicList(unfinishobj) 282 | for __i in range(len(unfinish_list)): 283 | unfinish_list[__i] = Music(unfinish_list[__i]) 284 | for __j in range(len(unfinish_list[__i].charts)): 285 | unfinish_list[__i].charts[__j] = Chart(unfinish_list[__i].charts[__j]) -------------------------------------------------------------------------------- /src/libraries/maimai/near_maimai.py: -------------------------------------------------------------------------------- 1 | import aiohttp,random,requests 2 | from src.libraries.GLOBAL_CONSTANT import API_KEY 3 | apikey = API_KEY 4 | alternate_apikey = [API_KEY] 5 | 6 | def reset_apikey(): 7 | global apikey 8 | apikey = API_KEY 9 | 10 | async def getLocation(): 11 | async with aiohttp.request('GET', 'http://wc.wahlap.net/maidx/rest/location') as resp: 12 | AddressData = await resp.json() 13 | return AddressData 14 | 15 | 16 | def route_analysis(listinfo): 17 | nf = listinfo['segments'] 18 | Msg = '首先冲到' 19 | for ddd in nf: 20 | for item in ddd.items(): 21 | # print(item[0]) 22 | if item[0] == 'bus': 23 | # print(item[1]['buslines'][0]) 24 | try: 25 | busname = item[1]['buslines'][0]['name'] 26 | start_action = item[1]['buslines'][0]['departure_stop']['name'] 27 | end_action = item[1]['buslines'][0]['arrival_stop']['name'] 28 | # print(start_action,end_action) 29 | Msg += f'{start_action}坐{busname}到{end_action}\n再' 30 | except: 31 | pass 32 | # print(item[1]) 33 | # if 34 | Msg += '应该就到了,你找找附近吧。' 35 | return Msg 36 | 37 | 38 | 39 | async def GetGoShopData(address:str): 40 | global apikey 41 | params = { 42 | 'key': apikey, 43 | 'address':address 44 | } 45 | async with aiohttp.request('GET',f'https://restapi.amap.com/v3/geocode/geo',params=params) as resp: 46 | result = await resp.json() 47 | if result['infocode'] == "10044": 48 | apikey = random.choice(alternate_apikey) 49 | result = requests.get(f'https://restapi.amap.com/v3/geocode/geo',params={'key': apikey,'address':address}).json() 50 | return result['geocodes'][0]['location'] 51 | 52 | 53 | 54 | async def GetGoOutList(cityshops:list,latitude,longitude,city:str): 55 | GoShopList = [] 56 | for address in cityshops: 57 | destination = await GetGoShopData(address['address']) 58 | params = { 59 | 'key': apikey, 60 | 'origin':f'{latitude},{longitude}', 61 | 'destination': destination, 62 | 'city':city 63 | } 64 | async with aiohttp.request('GET',f'https://restapi.amap.com/v3/direction/transit/integrated?',params=params) as resp: 65 | result = await resp.json() 66 | try: 67 | item = result['route']['transits'][0] 68 | except: 69 | GoShopList.append({'cost':'0','duration':0,'walking_distance':'0','name':'你脚下↓:'+address['mall'],'address':address['address'],'lacation':destination,'route_msg':''}) 70 | continue 71 | try: 72 | route_msg = route_analysis(item) 73 | except: 74 | GoShopList.append({'cost':'0','duration':0,'walking_distance':'0','name':address['mall'],'address':address['address'],'lacation':destination,'route_msg':''}) 75 | continue 76 | stime:int = int(item['duration']) 77 | duration = (stime//60)+1 if stime%60 != 0 else stime//60 78 | # print({'cost':item['cost'],'duration':duration,'walking_distance':item['walking_distance'],'address':address['address']}) 79 | GoShopList.append({'cost':item['cost'],'duration':duration,'walking_distance':item['walking_distance'],'name':address['mall'],'address':address['address'],'lacation':destination,'route_msg':route_msg}) 80 | return GoShopList 81 | 82 | 83 | async def GetCityShops(city:str,latitude,longitude): 84 | cityshops = [] 85 | MaiMai_Location = await getLocation() 86 | for item in MaiMai_Location: 87 | if city in item['address']: 88 | cityshops.append(item) 89 | return await GetGoOutList(cityshops,latitude,longitude,city) 90 | 91 | 92 | def MapCQ(name: str,Address: str,lat: str, lng: str): 93 | CqText = '[CQ:json,data={ "app": "com.tencent.map", "config": { "autosize": false, "forward": true, "type": "normal" }, "desc": "", "meta": { "Location.Search": { "address": "'+ Address +'", "from": "plusPanel", "id": "10471098104178669718", "lat": "'+ lat +'", "lng": "'+ lng +'", "name": "'+ name +'" } }, "prompt": "[应用]地图", "ver": "0.0.0.1", "view": "LocationShare" }]' 94 | return CqText 95 | 96 | async def get_send_result(result): 97 | SendList = [{"type": "node","data": {"name": "Xray Bot","uin": "2468519813","content": [{"type": "text","data": {"text": "最快出勤点如下:"}}]}}] 98 | BY_SendList = [{"type": "node","data": {"name": "Xray Bot","uin": "2468519813","content": [{"type": "text","data": {"text": "最快出勤点如下:"}}]}}] 99 | 100 | for item in sorted(result, key = lambda i: i['duration'])[:5]: 101 | SendList.append({"type": "node","data": {"name": "Xray Bot","uin": "2468519813","content": [{"type": "text","data": {"text": f"地址:{item['name']}\n预计通勤时间:{item['duration']}分钟\n预计花费:{item['cost']}元\n{item['route_msg']}"}}]}}) 102 | BY_SendList.append({"type": "node","data": {"name": "Xray Bot","uin": "2468519813","content": [{"type": "text","data": {"text": f"地址:{item['name']}\n预计通勤时间:{item['duration']}分钟\n预计花费:{item['cost']}元\n{item['route_msg']}"}}]}}) 103 | SendList.append({"type": "node","data": {"name": "Xray Bot","uin": "2468519813","content": MapCQ(item['name'],item['address'],str(item['lacation']).split(',')[1],str(item['lacation']).split(',')[0])}}) 104 | 105 | SendList.append({"type": "node","data": {"name": "Xray Bot","uin": "2468519813","content": [{"type": "text","data": {"text": "本功能由高达地图强力驱动,如有故障请联系机修。"}}]}}) 106 | BY_SendList.append({"type": "node","data": {"name": "Xray Bot","uin": "2468519813","content": [{"type": "text","data": {"text": "本功能由高达地图强力驱动,如有故障请联系机修。"}}]}}) 107 | return SendList,BY_SendList 108 | 109 | 110 | async def getCityName(latitude,longitude): 111 | params = { 112 | 'key': apikey, 113 | 'location': f'{latitude},{longitude}', 114 | 'radius': '1000', 115 | 'extensions': 'all' 116 | } 117 | async with aiohttp.request('GET',f'https://restapi.amap.com/v3/geocode/regeo?',params=params) as resp: 118 | result = await resp.json() 119 | # params(result['regeocode']['addressComponent']) 120 | if result['regeocode']['addressComponent'].get('city',[]): 121 | return await GetCityShops(result['regeocode']['addressComponent']['city'],latitude,longitude) 122 | else: 123 | return await GetCityShops(result['regeocode']['addressComponent']['province'],latitude,longitude) -------------------------------------------------------------------------------- /src/libraries/maimai/player_music_score/generate_utils.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | import requests 3 | from src.libraries.maimai.maimaidx_music import total_list 4 | from src.libraries.execution_time import timing_decorator_async 5 | from src.libraries.GLOBAL_CONSTANT import DEVELOPER_TOKEN 6 | 7 | def line_break(line,line_char_count:int,table_width:int): 8 | ret = '' 9 | width = 0 10 | for c in line: 11 | if len(c.encode('utf8')) == 3: # 中文 12 | if line_char_count == width + 1: # 剩余位置不够一个汉字 13 | width = 2 14 | ret += '\n' + c 15 | else: # 中文宽度加2,注意换行边界 16 | width += 2 17 | ret += c 18 | else: 19 | if c == '\t': 20 | space_c = table_width - width % table_width # 已有长度对TABLE_WIDTH取余 21 | ret += ' ' * space_c 22 | width += space_c 23 | elif c == '\n': 24 | width = 0 25 | ret += c 26 | else: 27 | width += 1 28 | ret += c 29 | if width >= line_char_count: 30 | ret += '\n' 31 | width = 0 32 | if ret.endswith('\n'): 33 | return ret 34 | return ret + '\n' 35 | 36 | # 辅助函数:分割文本以适应最大宽度 37 | def split_text_to_lines(text, max_width, font): 38 | lines = [] 39 | current_line = "" 40 | for char in text: 41 | # 检查当前行的宽度加上新字符的宽度是否超过最大宽度 42 | text_width, text_height = font.getsize(current_line + char) 43 | if text_width <= max_width: 44 | current_line += char 45 | else: 46 | # 如果超过最大宽度,则将当前行添加到列表中,并开始新行 47 | lines.append(current_line) 48 | current_line = char 49 | # 添加最后一行(如果有的话) 50 | if current_line: 51 | lines.append(current_line) 52 | return "\n".join(lines) 53 | 54 | async def get_user_all_records(music_id:str,qq:str = None,username:str = None): 55 | payload = get_request_payload(qq=qq,username=username) 56 | if payload == 400: 57 | return 40 58 | payload['music_id'] = [music_id] 59 | headers = { 60 | "developer-token": DEVELOPER_TOKEN 61 | } 62 | async with aiohttp.request("POST", "https://www.diving-fish.com/api/maimaidxprober/dev/player/record", json=payload,headers=headers) as resp: 63 | request_json = await resp.json() 64 | return resp.status,request_json 65 | 66 | def get_request_payload(qq:str = None,username:str = None): 67 | if qq is None and username is None: 68 | return 400 69 | else: 70 | if qq: 71 | payload = {"qq":qq} 72 | else: 73 | payload = {"username":username} 74 | return payload 75 | 76 | @timing_decorator_async 77 | async def get_user_music_score(music_id:int,qq:str = None,username:str = None): 78 | status_code,user_all_records = await get_user_all_records(str(music_id),qq=qq,username=username) 79 | if status_code!= 400: 80 | if str(music_id) in user_all_records: 81 | # filtered_data = [item for item in user_all_records['records'] if item['song_id'] == music_id] 82 | music_data = total_list.by_id(str(music_id)) 83 | player_music_score = {index:{} for index,ds in enumerate(music_data.level)} 84 | # print(player_music_score) 85 | for item in user_all_records[str(music_id)]: 86 | player_music_score[item['level_index']] = item 87 | return player_music_score 88 | else: 89 | music_data = total_list.by_id(str(music_id)) 90 | if len(music_data.level) < 4: 91 | player_music_score = {0:{},1:{},2:{},3:{}} 92 | else: 93 | player_music_score = {index:{} for index,ds in enumerate(music_data.level)} 94 | return player_music_score 95 | else: 96 | return 400 97 | -------------------------------------------------------------------------------- /src/libraries/maimai/player_music_score/player_music_score.py: -------------------------------------------------------------------------------- 1 | from src.libraries.maimai.player_music_score.generate_utils import get_user_music_score,line_break,split_text_to_lines 2 | from PIL import Image,ImageFont,ImageDraw 3 | from src.libraries.maimai.maimaidx_music import total_list 4 | from src.libraries.maimai.utils import get_cover_path 5 | from src.libraries.GLOBAL_CONSTANT import VERSION_LOGO_MAP 6 | from src.libraries.execution_time import timing_decorator,timing_decorator_async 7 | from src.libraries.GLOBAL_PATH import PLAYER_MUSIC_SCORE_PATH,FONT_PATH 8 | 9 | 10 | class PlayerMusicScore(): 11 | 12 | def __init__(self,music_id:int,player_music_score,is_abstract:bool) -> None: 13 | self.music_id = music_id 14 | self.music_info = total_list.by_id(str(music_id)) 15 | self.player_music_score = player_music_score 16 | self.is_abstract = is_abstract 17 | 18 | self.baseImg = Image.new("RGBA", (1700, 1800), (0, 0, 0, 0)) 19 | self.baseImgDraw = ImageDraw.Draw(self.baseImg) 20 | 21 | self.pic_dir = PLAYER_MUSIC_SCORE_PATH 22 | self.font_path = f'{FONT_PATH}/GlowSansSC-Normal-Bold.otf' 23 | self.title_font_path = f'{FONT_PATH}/SourceHanSans_35.otf' 24 | 25 | def getRankIcon(self,achievements:str): 26 | achievements = float(achievements) 27 | if achievements >= 100.5: 28 | return 'sssp.png' 29 | elif achievements >= 100: 30 | return 'sss.png' 31 | elif achievements >= 99.5: 32 | return 'ssp.png' 33 | elif achievements >= 99: 34 | return 'ss.png' 35 | elif achievements >= 98: 36 | return 'sp.png' 37 | elif achievements >= 97: 38 | return 's.png' 39 | elif achievements >= 94: 40 | return 'aaa.png' 41 | elif achievements >= 90: 42 | return 'aa.png' 43 | elif achievements >= 80: 44 | return 'a.png' 45 | elif achievements >= 75: 46 | return 'bbb.png' 47 | elif achievements >= 70: 48 | return 'bb.png' 49 | elif achievements >= 60: 50 | return 'b.png' 51 | elif achievements >= 50: 52 | return 'c.png' 53 | else: 54 | return 'd.png' 55 | 56 | def _getCharWidth(self, o) -> int: 57 | widths = [ 58 | (126, 1), (159, 0), (687, 1), (710, 0), (711, 1), (727, 0), (733, 1), (879, 0), (1154, 1), (1161, 0), 59 | (4347, 1), (4447, 2), (7467, 1), (7521, 0), (8369, 1), (8426, 0), (9000, 1), (9002, 2), (11021, 1), 60 | (12350, 2), (12351, 1), (12438, 2), (12442, 0), (19893, 2), (19967, 1), (55203, 2), (63743, 1), 61 | (64106, 2), (65039, 1), (65059, 0), (65131, 2), (65279, 1), (65376, 2), (65500, 1), (65510, 2), 62 | (120831, 1), (262141, 2), (1114109, 1), 63 | ] 64 | if o == 0xe or o == 0xf: 65 | return 0 66 | for num, wid in widths: 67 | if o <= num: 68 | return wid 69 | return 1 70 | 71 | def _coloumWidth(self, s:str): 72 | res = 0 73 | for ch in s: 74 | res += self._getCharWidth(ord(ch)) 75 | return res 76 | 77 | def _changeColumnWidth(self, s:str, len:int) -> str: 78 | res = 0 79 | sList = [] 80 | for ch in s: 81 | res += self._getCharWidth(ord(ch)) 82 | if res <= len: 83 | sList.append(ch) 84 | return ''.join(sList) 85 | 86 | def _get_dxscore_type(self,dxscorepen): 87 | if dxscorepen <= 85: 88 | return "0" 89 | elif dxscorepen <= 90: 90 | return "1" 91 | elif dxscorepen <= 93: 92 | return "2" 93 | elif dxscorepen <= 95: 94 | return "3" 95 | elif dxscorepen <= 97: 96 | return "4" 97 | else: 98 | return "5" 99 | 100 | @timing_decorator 101 | def draw(self): 102 | cover_path = get_cover_path(self.music_id,self.is_abstract) 103 | musicCoverImg = Image.open(cover_path).convert('RGBA') 104 | musicCoverImg = musicCoverImg.resize((494,494)) 105 | self.baseImg.paste(musicCoverImg,(191,172),musicCoverImg.split()[3]) 106 | 107 | 108 | backageImg = Image.open(f"{self.pic_dir}/background.png").convert('RGBA') 109 | self.baseImg.paste(backageImg,(0,0),backageImg.split()[3]) 110 | 111 | # 添加曲师 112 | tempFont = ImageFont.truetype(self.font_path, 43, encoding='utf-8') 113 | artist = self.music_info.artist 114 | if self._coloumWidth(artist) > 38: 115 | artist = self._changeColumnWidth(artist, 34) + '...' 116 | self.baseImgDraw.text((729,180), artist , "white" , tempFont) 117 | 118 | # Title 119 | tempFont = ImageFont.truetype(self.title_font_path, 60, encoding='utf-8') 120 | title = split_text_to_lines(self.music_info.title,800,tempFont) 121 | # title = line_break(self.music_info.title,26,4) 122 | self.baseImgDraw.text((727,246), title , "white" , tempFont) 123 | 124 | # music_id bpm 分类 版本 125 | tempFont = ImageFont.truetype(self.font_path, 45, encoding='utf-8') 126 | 127 | text_width, text_height = tempFont.getsize(self.music_info.id) 128 | self.baseImgDraw.text(((810-text_width/2),(610-text_height/2)), self.music_info.id , "white" , tempFont) 129 | 130 | text_width, text_height = tempFont.getsize(str(self.music_info.bpm)) 131 | self.baseImgDraw.text(((953-text_width/2),(610-text_height/2)), str(self.music_info.bpm) , "white" , tempFont) 132 | 133 | if self.music_info.genre in ["niconico & VOCALOID","niconicoボーカロイド","ゲームバラエティ"]: 134 | tempFont = ImageFont.truetype(self.font_path, 30, encoding='utf-8') 135 | text_width, text_height = tempFont.getsize(self.music_info.genre) 136 | self.baseImgDraw.text(((1194-text_width/2),(613-text_height/2)), self.music_info.genre , "white" , tempFont) 137 | else: 138 | text_width, text_height = tempFont.getsize(self.music_info.genre) 139 | self.baseImgDraw.text(((1194-text_width/2),(614-text_height/2)), self.music_info.genre , "white" , tempFont) 140 | 141 | # 类型 142 | TypeIconImg = Image.open(f"{self.pic_dir}/类型/{self.music_info.type}.png").convert('RGBA') 143 | self.baseImg.paste(TypeIconImg,(1393,597),TypeIconImg.split()[3]) 144 | 145 | # logo 146 | if self.music_info.cn_version in VERSION_LOGO_MAP: 147 | VersionLogoImg = Image.open(f"{self.pic_dir}/版本牌/UI_CMN_TabTitle_MaimaiTitle_Ver{VERSION_LOGO_MAP.get(self.music_info.cn_version)}.png").convert('RGBA') 148 | text_width, text_height = VersionLogoImg.size 149 | self.baseImg.paste(VersionLogoImg,(250-int(text_width/2),150-int(text_height/2)),VersionLogoImg.split()[3]) 150 | 151 | 152 | 153 | if 4 not in self.player_music_score: 154 | maskImg = Image.open(f"{self.pic_dir}/MASK/mask_2.png").convert('RGBA') 155 | self.baseImg.paste(maskImg,(191,751+158*4),maskImg.split()[3]) 156 | 157 | score_box_y_map = { 158 | 0:751, 159 | 1:751+158*1, 160 | 2:751+158*2, 161 | 3:751+158*3, 162 | 4:751+158*4 163 | } 164 | for k,v in self.player_music_score.items(): 165 | if not v: 166 | maskImg = Image.open(f"{self.pic_dir}/MASK/mask_1.png").convert('RGBA') 167 | self.baseImg.paste(maskImg,(191,score_box_y_map.get(k)),maskImg.split()[3]) 168 | else: 169 | baseBoxImg = Image.new("RGBA", (1321, 126), (0, 0, 0, 0)) 170 | 171 | if v['fc']: 172 | fcIconImg = Image.open(f"{self.pic_dir}/PlayBonus/{str(v['fc']).upper()}.png").convert('RGBA') 173 | baseBoxImg.paste(fcIconImg,(0,0),fcIconImg.split()[3]) 174 | 175 | if v['fs']: 176 | if v['fs'].upper() != 'SYNC': 177 | fsIconImg = Image.open(f"{self.pic_dir}/PlayBonus/{str(v['fs']).upper()}.png").convert('RGBA') 178 | baseBoxImg.paste(fsIconImg,(0,0),fsIconImg.split()[3]) 179 | 180 | rankIconImg = Image.open(f"{self.pic_dir}/RANK/{str(v['rate']).upper()}.png").convert('RGBA') 181 | baseBoxImg.paste(rankIconImg,(0,2),rankIconImg.split()[3]) 182 | 183 | start_mun = v['dxScore'] / (sum(self.music_info.charts[k]['notes'])*3) *100 184 | star_index = self._get_dxscore_type(start_mun) 185 | if star_index != "0": 186 | MusicdxStarIcon = Image.open(f"{self.pic_dir}/星级/Star_{star_index}.png").convert('RGBA') 187 | baseBoxImg.paste(MusicdxStarIcon,(0,0),MusicdxStarIcon.split()[3]) 188 | 189 | baseBoxImgDraw = ImageDraw.Draw(baseBoxImg) 190 | 191 | achievements = "%.4f" % v['achievements'] 192 | tempFont = ImageFont.truetype(self.font_path, 62, encoding='utf-8') 193 | text_width, text_height = tempFont.getsize(achievements) 194 | baseBoxImgDraw.text((643-int(text_width/2),52-int(text_height/2)), achievements, "black" , tempFont) 195 | 196 | # 单曲rat+ds 197 | content = str(self.music_info.ds[k]) + '→' + str(v['ra']) 198 | tempFont = ImageFont.truetype(self.font_path, 30, encoding='utf-8') 199 | text_width, text_height = tempFont.getsize(content) 200 | baseBoxImgDraw.text((1205-int(text_width/2),32-int(text_height/2)), content, "black" , tempFont) 201 | 202 | # dx分 203 | content = str(v['dxScore']) + '/' + str((sum(self.music_info.charts[k]['notes'])*3)) 204 | tempFont = ImageFont.truetype(self.font_path, 30, encoding='utf-8') 205 | text_width, text_height = tempFont.getsize(content) 206 | baseBoxImgDraw.text((1205-int(text_width/2),86-int(text_height/2)), content, "black" , tempFont) 207 | 208 | 209 | self.baseImg.paste(baseBoxImg,(191,score_box_y_map.get(k)),baseBoxImg.split()[3]) 210 | 211 | 212 | 213 | return self.baseImg 214 | 215 | @timing_decorator_async 216 | async def generate_info(music_id:int,user_id:str,is_abstract:bool): 217 | player_music_score = await get_user_music_score(music_id,qq=user_id) 218 | if player_music_score != 400: 219 | player_music_score_img = PlayerMusicScore(music_id=music_id,player_music_score=player_music_score,is_abstract=is_abstract).draw() 220 | return True,player_music_score_img 221 | else: 222 | return False,None -------------------------------------------------------------------------------- /src/libraries/maimai/utils.py: -------------------------------------------------------------------------------- 1 | from src.libraries.data_handle.abstract_db_handle import abstract 2 | from pathlib import Path 3 | from src.libraries.GLOBAL_PATH import ABSTRACT_COVER_PATH,NORMAL_COVER_PATH,ABSTRACT_DOWNLOAD_URL 4 | import requests 5 | 6 | 7 | def download_image(url, save_path): 8 | try: 9 | # 发送HTTP GET请求 10 | response = requests.get(url) 11 | # 确保请求成功 12 | response.raise_for_status() 13 | 14 | # 将图片内容写入到本地文件 15 | with open(save_path, 'wb') as file: 16 | file.write(response.content) 17 | 18 | print(f"图片已保存到 {save_path}") 19 | except requests.RequestException as e: 20 | print(e) 21 | 22 | 23 | 24 | def check_file_and_save_img(file_name): 25 | file_path = Path(ABSTRACT_COVER_PATH + f'/{str(file_name)}.png') 26 | if not file_path.exists(): 27 | url = ABSTRACT_DOWNLOAD_URL + f'/{file_name}.png' 28 | download_image(url,file_path) 29 | else: 30 | return '存在' 31 | 32 | def get_abstract_cover_path_by_file_id(file_name): 33 | CoverPath = ABSTRACT_COVER_PATH + f'/{file_name}.png' 34 | check_file_and_save_img(file_name) 35 | return CoverPath 36 | 37 | 38 | def get_cover_path(song_id,is_abstract:bool): 39 | id = int(song_id) 40 | if is_abstract: 41 | file_name,nickname = abstract.get_abstract_file_name(id) 42 | if nickname != "抽象画未收录": 43 | CoverPath = ABSTRACT_COVER_PATH + f'/{file_name}.png' 44 | check_file_and_save_img(file_name) 45 | else: 46 | CoverPath = f'{NORMAL_COVER_PATH}/{id}.png' 47 | else: 48 | CoverPath = f'{NORMAL_COVER_PATH}/{id}.png' 49 | 50 | if Path(CoverPath).exists(): 51 | return CoverPath 52 | else: 53 | if Path(NORMAL_COVER_PATH + f'/{id-10000}.png').exists(): 54 | return NORMAL_COVER_PATH + f'/{id-10000}.png' 55 | if Path(NORMAL_COVER_PATH + f'/{id+10000}.png').exists(): 56 | return NORMAL_COVER_PATH + f'/{id+10000}.png' 57 | if Path(NORMAL_COVER_PATH + f'/{id-100000}.png').exists(): 58 | return NORMAL_COVER_PATH + f'/{id-100000}.png' 59 | if Path(NORMAL_COVER_PATH + f'/{id-110000}.png').exists(): 60 | return NORMAL_COVER_PATH + f'/{id-110000}.png' 61 | return f'{ABSTRACT_COVER_PATH}/1000.jpg' -------------------------------------------------------------------------------- /src/plugins/xray_plugin_black_list/__init__.py: -------------------------------------------------------------------------------- 1 | from src.libraries.GLOBAL_CONSTANT import BOT_DATA_STAFF 2 | from src.libraries.GLOBAL_RULE import check_is_bot_data_staff 3 | from nonebot import on_command 4 | from nonebot.adapters.onebot.v11 import Message ,MessageEvent,GroupMessageEvent,Bot 5 | from nonebot.plugin import on_command 6 | from nonebot.params import CommandArg 7 | from src.libraries.data_handle.black_list_handle import admin 8 | from nonebot.log import logger 9 | import contextlib 10 | from typing import Dict, Any 11 | from nonebot.exception import IgnoredException 12 | from nonebot.message import event_preprocessor 13 | from src.plugins.xray_plugins_log import c_logger 14 | 15 | 16 | add_group_white = on_command('开启群', rule=check_is_bot_data_staff(),priority=3) 17 | delete_group_white = on_command('关闭群', rule=check_is_bot_data_staff(),priority=3) 18 | add_user_black = on_command('拉黑', rule=check_is_bot_data_staff(),priority=3) 19 | delete_user_black = on_command('解黑', rule=check_is_bot_data_staff(),priority=3) 20 | 21 | def execut_event_message(event:MessageEvent): 22 | data = event.message 23 | msglist = [] 24 | for item in data: 25 | if item.type == 'text': 26 | msg = item.data['text'] 27 | msglist.append(msg) 28 | else: 29 | msglist.append(f"【{item.type}】") 30 | msg = ''.join(msglist) 31 | log_data = {"type":"message_event"} 32 | if isinstance(event,GroupMessageEvent): 33 | log_data["group_id"] = event.group_id 34 | log_data['user_id'] = event.user_id 35 | log_data['content'] = msg 36 | c_logger.debug(log_data) 37 | 38 | @Bot.on_calling_api 39 | async def __(bot: Bot, api: str, data: Dict[str, Any]): 40 | with contextlib.suppress(Exception): 41 | if api not in ['send_msg', 'send_message']: 42 | return 43 | msglist = [] 44 | for item in data['message']: 45 | if item.type == 'text': 46 | msg = item.data['text'] 47 | msglist.append(msg) 48 | else: 49 | msglist.append(f"【{item.type}】") 50 | msg = ''.join(msglist) 51 | c_logger.info({ 52 | "type":"send_message", 53 | "bot_id":bot.self_id, 54 | "user_id":data['user_id'], 55 | "group_id":data['group_id'], 56 | "content":msg 57 | }) 58 | 59 | # 取消注释开启黑白名单 60 | 61 | # @event_preprocessor 62 | # def blacklist_processor(event: MessageEvent): 63 | # is_block = 0 64 | # if isinstance(event,GroupMessageEvent): 65 | # if event.group_id in admin.get_groupid(): 66 | # is_block = 0 67 | # else: 68 | # is_block = 1 69 | # if event.user_id in admin.get_userid(): 70 | # is_block = 1 71 | # if event.user_id in BOT_DATA_STAFF: 72 | # is_block = 0 73 | 74 | # if is_block: 75 | # logger.success(f'拒绝开启会话') 76 | # raise IgnoredException('黑名单会话') 77 | # else: 78 | # execut_event_message(event) 79 | # return 80 | 81 | @add_group_white.handle() 82 | async def __(event: MessageEvent, args:Message=CommandArg()): 83 | groupid = int(str(args).strip()) 84 | await add_group_white.send(admin.add_group(groupid)) 85 | 86 | @delete_group_white.handle() 87 | async def __(event: MessageEvent, args:Message=CommandArg()): 88 | groupid = int(str(args).strip()) 89 | await delete_group_white.send(admin.del_group(groupid)) 90 | 91 | @add_user_black.handle() 92 | async def __(event: MessageEvent, args:Message=CommandArg()): 93 | userid = int(str(args).strip()) 94 | await add_user_black.send(admin.add_user(userid)) 95 | 96 | @delete_user_black.handle() 97 | async def __(event: MessageEvent, args:Message=CommandArg()): 98 | userid = int(str(args).strip()) 99 | await add_user_black.send(admin.del_user(userid)) -------------------------------------------------------------------------------- /src/plugins/xray_plugin_guess_music/__init__.py: -------------------------------------------------------------------------------- 1 | from nonebot import on_message,on_shell_command 2 | from nonebot.adapters.onebot.v11 import GroupMessageEvent, Message,MessageSegment,Bot 3 | from nonebot.params import ShellCommandArgs 4 | from nonebot.rule import Rule 5 | import random 6 | from nonebot.typing import T_State 7 | import asyncio 8 | import datetime 9 | from src.libraries.data_handle.alias_db_handle import alias 10 | from src.libraries.data_handle.abstract_db_handle import abstract 11 | from src.libraries.maimai.maimaidx_music import obj,total_list 12 | from typing import Dict 13 | from src.libraries.image_handle.image import * 14 | from PIL import Image, ImageFilter 15 | from nonebot.rule import Namespace, ArgumentParser 16 | from src.libraries.maimai.utils import get_abstract_cover_path_by_file_id 17 | 18 | hot_music_ids = [] 19 | music_ids = abstract.get_abstract_id_list() 20 | for item in music_ids: 21 | hot_music_ids.append(str(item)) 22 | 23 | 24 | my_data = list(filter(lambda x: x['id'] in hot_music_ids, obj)) 25 | 26 | guess_music_dict = {} 27 | 28 | def check_Admin(): 29 | async def _checker(event: GroupMessageEvent) -> bool: 30 | if event.user_id in [381268035,3434334409]: 31 | return True 32 | else: 33 | return False 34 | return Rule(_checker) 35 | 36 | def Get_Cut_Img(file_name,mode) -> None: 37 | IMG_data = get_abstract_cover_path_by_file_id(file_name) 38 | img = Image.open(IMG_data).convert("RGBA") 39 | if mode: 40 | img = img.resize((400,400)) 41 | img = img.filter(random.choice([ImageFilter.CONTOUR(),ImageFilter.GaussianBlur(7)])) 42 | else: 43 | img = img.resize((300,300)) 44 | w, h = img.size 45 | w2, h2 = int(w / 3), int(h / 3) 46 | l, u = random.randrange(0, int(2 * w / 3)), random.randrange(0, int(2 * h / 3)) 47 | img = img.crop((l, u, l+w2, u+h2)) 48 | b64image = image_to_base64(img) 49 | return b64image 50 | 51 | async def guess_music_loop(bot: Bot, event: GroupMessageEvent, state: T_State,mode:0): 52 | # print(my_data) 53 | music: Dict = random.choice(my_data) 54 | file_name,nickname = abstract.get_abstract_file_name(str(music['id'])) 55 | 56 | b64image = Get_Cut_Img(file_name,mode) 57 | last_time = (datetime.datetime.now() + datetime.timedelta(seconds= +30)).strftime('%Y-%m-%d %H:%M:%S') 58 | 59 | guess_music_dict[event.group_id] = {"music":music,"end_time":last_time,"is_end":0,"mode":mode,"file_name":file_name} 60 | asyncio.create_task(bot.send(event, Message([ 61 | MessageSegment.text("这首歌封面的一部分是:"), 62 | MessageSegment.image("base64://" + str(b64image, encoding="utf-8")), 63 | MessageSegment.text("答案将在 30 秒后揭晓") 64 | ]))) 65 | asyncio.create_task(give_answer(bot, event, state)) 66 | return 67 | 68 | async def give_answer(bot: Bot, event: GroupMessageEvent, state: T_State): 69 | for item in range(30): 70 | await asyncio.sleep(1) 71 | if not guess_music_dict.get(event.group_id,0): 72 | return 73 | music = guess_music_dict[event.group_id]['music'] 74 | file_name = guess_music_dict[event.group_id]['file_name'] 75 | IMG_data = get_abstract_cover_path_by_file_id(file_name) 76 | with open(IMG_data, mode="rb") as f: 77 | data = f.read() 78 | asyncio.create_task(bot.send(event, Message([MessageSegment.text("答案是:" + f"{music['id']}. {music['title']}\n"),MessageSegment.image(data)]))) 79 | guess_music_dict.pop(event.group_id) 80 | 81 | search_location_parser = ArgumentParser() 82 | search_location_parser.add_argument("-hd", default=False, action='store_true') 83 | 84 | 85 | guess_music = on_shell_command('猜曲绘', priority=10,parser=search_location_parser) 86 | 87 | @guess_music.handle() 88 | async def _(bot: Bot, event: GroupMessageEvent, state: T_State,foo: Namespace = ShellCommandArgs()): 89 | group_id = event.group_id 90 | if not guess_music_dict.get(group_id,0): 91 | asyncio.create_task(guess_music_loop(bot, event, state,foo.hd)) 92 | else: 93 | await guess_music.send("当前已有正在进行的猜曲绘") 94 | 95 | guess_music_solve = on_message(priority=8, block=False) 96 | @guess_music_solve.handle() 97 | async def _(bot: Bot, event: GroupMessageEvent, state: T_State): 98 | if guess_music_dict.get(event.group_id,0): 99 | ans = str(event.get_message()) 100 | music = guess_music_dict[event.group_id]["music"] 101 | ermnade = False 102 | name = str(event.get_message()).strip().lower() 103 | result_set = alias.queryMusicByAlias(name) 104 | 105 | if len(result_set) == 1: 106 | music_result = total_list.by_id(result_set[0]) 107 | if music['id'] == music_result.id: 108 | ermnade = True 109 | elif len(result_set) == 0: 110 | ermnade = False 111 | else: 112 | for result in result_set: 113 | music_result = total_list.by_id(result) 114 | if music['id'] == music_result.id: 115 | ermnade = True 116 | break 117 | if str(event.message) == "开挂" and event.user_id in [381268035]: 118 | cheat = 1 119 | else: 120 | cheat = 0 121 | 122 | if str(event.message) == "跳过": 123 | await guess_music_solve.send([MessageSegment.text("你们真TM菜😶‍🌫️")]) 124 | mode = guess_music_dict[event.group_id]['mode'] 125 | 126 | music = guess_music_dict[event.group_id]['music'] 127 | file_name = guess_music_dict[event.group_id]['file_name'] 128 | IMG_data = get_abstract_cover_path_by_file_id(file_name) 129 | guess_music_dict.pop(event.group_id) 130 | 131 | with open(IMG_data, mode="rb") as f: 132 | data = f.read() 133 | await guess_music_solve.send([MessageSegment.text("答案是:" + f"{music['id']}. {music['title']}\n"),MessageSegment.image(data)]) 134 | 135 | 136 | if cheat or ermnade or ans == music['id'] or (ans.lower() == music['title'].lower()) or (len(ans) >= 5 and ans.lower() in music['title'].lower()): 137 | mode = guess_music_dict[event.group_id]['mode'] 138 | file_name = guess_music_dict[event.group_id]['file_name'] 139 | guess_music_dict.pop(event.group_id) 140 | with open(get_abstract_cover_path_by_file_id(file_name), mode="rb") as f: 141 | data = f.read() 142 | await guess_music_solve.send(Message([ 143 | MessageSegment.reply(event.message_id), 144 | MessageSegment.text("猜对了,答案是:" + f"{music['id']}. {music['title']}\n"), 145 | MessageSegment.image(data) 146 | ])) 147 | group_id = event.group_id 148 | if not guess_music_dict.get(group_id,0): 149 | # group_Data = guess_music_dict.get(group_id,0) 150 | asyncio.create_task(guess_music_loop(bot, event, state,mode)) 151 | else: 152 | return 153 | 154 | 155 | -------------------------------------------------------------------------------- /src/plugins/xray_plugin_maimaidx/utils.py: -------------------------------------------------------------------------------- 1 | from src.libraries.maimai.maimai_music_data.maimai_utage_music_data import MaiUtageMusicData 2 | from src.libraries.maimai.maimaidx_music import total_list,Music 3 | from src.libraries.image_handle.image import image_to_base64 4 | from nonebot.adapters.onebot.v11 import MessageSegment 5 | from src.libraries.maimai.maimai_music_data.maimai_music_data import MaiMusicData 6 | import requests 7 | import aiohttp 8 | import json 9 | from src.libraries.GLOBAL_PATH import MAIDX_LOCATION_PATH 10 | from src.libraries.GLOBAL_CONSTANT import VERSION_MAP,VERSION_DF_MAP 11 | import operator 12 | 13 | def inner_level_q(ds1, ds2=None): 14 | result_set = [] 15 | diff_label = ['Bas', 'Adv', 'Exp', 'Mst', 'ReM'] 16 | if ds2 is not None: 17 | music_data = total_list.filter(ds=(ds1, ds2)) 18 | else: 19 | music_data = total_list.filter(ds=ds1) 20 | for music in music_data: 21 | for i in music.diff: 22 | result_set.append( 23 | (music['id'], music['title'], music['ds'][i], diff_label[i], music['level'][i])) 24 | return result_set 25 | 26 | 27 | def music_image(music: Music, is_abstract: bool): 28 | if int(music.id) >= 100000: 29 | music_data_img = MaiUtageMusicData(int(music.id), is_abstract).draw() 30 | else: 31 | music_data_img = MaiMusicData(int(music.id), is_abstract).draw() 32 | return [ 33 | MessageSegment.image(f"base64://{str(image_to_base64(music_data_img), encoding='utf-8')}")] 34 | 35 | def query_PANDORA_PARADOXXX_score_content(qq): 36 | payload = {'qq': qq, 'version': ["maimai FiNALE"]} 37 | r = requests.post("https://www.diving-fish.com/api/maimaidxprober/query/plate", json=payload) 38 | verlist = r.json() 39 | song_Info_list = {} 40 | for item in verlist['verlist']: 41 | if item['id'] == 834: 42 | song_Info_list[item['level_index']] = item 43 | bas_ach = song_Info_list.get(0, {'achievements': -1}).get('achievements', -1) 44 | adv_ach = song_Info_list.get(1, {'achievements': -1}).get('achievements', -1) 45 | ex_ach = song_Info_list.get(2, {'achievements': -1}).get('achievements', -1) 46 | mas_ach = song_Info_list.get(3, {'achievements': -1}).get('achievements', -1) 47 | remas_ach = song_Info_list.get(4, {'achievements': -1}).get('achievements', -1) 48 | msg = '你好\n' 49 | 50 | if bas_ach == -1: 51 | msg += f'你的绿潘成绩:未游玩\n' 52 | else: 53 | msg += f'你的绿潘成绩是:{bas_ach}\n' 54 | 55 | if adv_ach == -1: 56 | msg += f'你的黄潘成绩:未游玩\n' 57 | else: 58 | msg += f'你的黄潘成绩是:{adv_ach}\n' 59 | 60 | if ex_ach == -1: 61 | msg += f'你的红潘成绩:未游玩\n' 62 | else: 63 | msg += f'你的红潘成绩是:{ex_ach}\n' 64 | 65 | if mas_ach == -1: 66 | msg += f'你的紫潘成绩:未游玩\n' 67 | else: 68 | msg += f'你的紫潘成绩是:{mas_ach}\n' 69 | 70 | if remas_ach == -1: 71 | msg += f'你的白潘成绩:未游玩\n' 72 | else: 73 | msg += f'你的白潘成绩是:{remas_ach}\n' 74 | if len(msg) > 5: 75 | return msg 76 | else: 77 | return 0 78 | 79 | async def get_maidx_location(): 80 | async with aiohttp.request('GET', 'http://wc.wahlap.net/maidx/rest/location') as resp: 81 | AddressData = await resp.json() 82 | return AddressData 83 | 84 | def read_location_json(): 85 | data = open(MAIDX_LOCATION_PATH, encoding='utf-8') 86 | strJson = json.load(data) 87 | return strJson 88 | 89 | 90 | def findsong_byid(id,index,list): 91 | for item in list: 92 | if item["id"] == id and item['level_index'] == index: 93 | return item 94 | return None 95 | 96 | def query_user_version_plate_schedule_result(version, qq: str, plateType: str, vername: str): 97 | version_list = total_list.by_versions_for_cn(VERSION_MAP[version]) 98 | version_id_list = [] 99 | for song in version_list: 100 | version_id_list.append(int(song['id'])) 101 | 102 | payload = {'qq': qq, 'version': VERSION_DF_MAP[version]} 103 | r = requests.post("https://www.diving-fish.com/api/maimaidxprober/query/plate", json=payload) 104 | finishs = r.json() 105 | unfinishList = {0: [], 1: [], 2: [], 3: [], 4: []} 106 | 107 | for song in version_list: 108 | songid = int(song['id']) 109 | for index in range(len(song['level'])): 110 | song_result = findsong_byid(songid, index, finishs['verlist']) 111 | if song_result: 112 | if plateType == '将': 113 | if song_result['achievements'] < 100: 114 | unfinishList[index].append(song_result) 115 | elif plateType == '极': 116 | if song_result['fc'] not in ['fc', 'ap', 'fcp', 'app']: 117 | unfinishList[index].append(song_result) 118 | elif plateType == '神': 119 | if song_result['fc'] not in ['ap', 'app']: 120 | unfinishList[index].append(song_result) 121 | elif plateType == '舞舞': 122 | if song_result['fs'] not in ['fsd', 'fsdp']: 123 | unfinishList[index].append(song_result) 124 | elif plateType == '者': 125 | if song_result['achievements'] < 80: 126 | unfinishList[index].append(song_result) 127 | else: 128 | unfinishList[index].append(song) 129 | 130 | # 高难度铺面 131 | HardSong = '' 132 | for item in unfinishList[3]: 133 | if item.get('achievements', -1) >= 0: 134 | # print(item['level'],1,type(item['level'])) 135 | if item['level'] in ['13+', '14', '14+', '15']: 136 | socre = str(item['achievements']) 137 | HardSong += ' ' + str(item['id']) + '.' + item['title'] + '(' + item['level'] + ') ' + socre + '\n' 138 | else: 139 | # print(item['level'],2) 140 | if item['level'][3] in ['13+', '14', '14+', '15']: 141 | socre = '未游玩' 142 | HardSong += ' ' + str(item['id']) + '.' + item['title'] + '(' + item['level'][3] + ') ' + socre + '\n' 143 | if vername in ['舞', '霸']: 144 | for item in unfinishList[4]: 145 | if item.get('achievements', -1) >= 0: 146 | # print(item['level'],1,type(item['level'])) 147 | if item['level'] in ['13+', '14', '14+', '15']: 148 | socre = str(item['achievements']) 149 | HardSong += ' ' + str(item['id']) + '.' + item['title'] + '(' + item[ 150 | 'level'] + ') ' + socre + '\n' 151 | else: 152 | # print(item['level'],2) 153 | if item['level'][3] in ['13+', '14', '14+', '15']: 154 | socre = '未游玩' 155 | HardSong += ' ' + str(item['id']) + '.' + item['title'] + '(' + item['level'][ 156 | 3] + ') ' + socre + '\n' 157 | t = vername + plateType 158 | SendMsg = f'你的{t}剩余进度如下:\n' 159 | unfinishSongCount = len(unfinishList[0]) + len(unfinishList[1]) + len(unfinishList[2]) + len( 160 | unfinishList[3]) if vername not in ['舞', '者'] else len(unfinishList[0]) + len(unfinishList[1]) + len( 161 | unfinishList[2]) + len(unfinishList[3]) + len(unfinishList[4]) 162 | unfinishGCount = len(unfinishList[0]) 163 | unfinishYCount = len(unfinishList[1]) 164 | unfinishRCount = len(unfinishList[2]) 165 | unfinishPCount = len(unfinishList[3]) 166 | unfinishREPCount = len(unfinishList[4]) 167 | if unfinishSongCount == 0: 168 | return f'您的{t}进度已经全部完成!!!\n' 169 | if unfinishGCount == 0: 170 | SendMsg += 'Basic难度已经全部完成\n' 171 | else: 172 | SendMsg += f'Basic剩余{str(unfinishGCount)}首\n' 173 | if unfinishYCount == 0: 174 | SendMsg += 'Advanced难度已经全部完成\n' 175 | else: 176 | SendMsg += f'Advanced剩余{str(unfinishYCount)}首\n' 177 | if unfinishRCount == 0: 178 | SendMsg += 'Expert难度已经全部完成\n' 179 | else: 180 | SendMsg += f'Expert剩余{str(unfinishRCount)}首\n' 181 | if unfinishPCount == 0: 182 | SendMsg += f'Master难度已经全部完成\n你已经{t}确认了!!!!\n' 183 | else: 184 | SendMsg += f'Master剩余{str(unfinishPCount)}首\n' 185 | if vername in ['舞', '霸']: 186 | if unfinishREPCount == 0: 187 | SendMsg += f'Re_Master难度已经全部完成\n你已经{t}确认了!!!!\n' 188 | else: 189 | SendMsg += f'Re_Master剩余{str(unfinishREPCount)}首\n' 190 | SendMsg += f'总共剩余{str(unfinishSongCount)}首\n' 191 | # print(unfinishRCount,unfinishPCount) 192 | if (unfinishRCount != 0 or unfinishPCount != 0) and vername not in ["舞", "霸"]: 193 | # print('Join') 194 | SendMsg += '未完成高难度谱面还剩下\n' 195 | SendMsg += HardSong[0:-1] 196 | lxzt = (unfinishSongCount // 4) + 1 if unfinishSongCount % 4 != 0 else unfinishSongCount // 4 197 | dszt = (unfinishSongCount // 3) + 1 if unfinishSongCount % 3 != 0 else unfinishSongCount // 3 198 | if plateType == "舞舞": 199 | SendMsg += f'\n单刷需要{str(dszt)}批西' 200 | else: 201 | SendMsg += f'\n贫瘠状态下需要{str(lxzt)}批西,单刷需要{str(dszt)}批西' 202 | SendMsg += '\n加油嗷!!!' 203 | return SendMsg 204 | 205 | 206 | async def getUsername(payload): 207 | async with aiohttp.request("POST", "https://www.diving-fish.com/api/maimaidxprober/query/player", 208 | json=payload) as resp: 209 | if resp.status == 400: 210 | return 400 211 | elif resp.status == 403: 212 | return 403 213 | elif resp.status == 200: 214 | best_info_Dict = await resp.json() 215 | username = str(best_info_Dict['username']) 216 | return username 217 | else: 218 | return -1 219 | 220 | async def getLowMsg(username: str, mode: int): 221 | async with aiohttp.request("get", 'https://www.diving-fish.com/api/maimaidxprober/rating_ranking') as resp: 222 | ranking_Dict = await resp.json() 223 | count = len(ranking_Dict) 224 | sum = 0 225 | top = -1 226 | ra = 0 227 | ranking_Dict = sorted(ranking_Dict, key=operator.itemgetter('ra'), reverse=True) 228 | if mode == 1: 229 | for index, item in enumerate(ranking_Dict): 230 | if str(item['username']).lower() == username: 231 | top = index + 1 232 | ra = item['ra'] 233 | sum += item['ra'] 234 | else: 235 | for index, item in enumerate(ranking_Dict): 236 | if item['username'] == username: 237 | top = index + 1 238 | ra = item['ra'] 239 | sum += item['ra'] 240 | if top == -1: 241 | return f'未找到{username},可能设置隐私。' 242 | avg = f'{sum / count:.4f}' 243 | topp = f'{((count - top) / count):.2%}' 244 | msg = f'{username}的底分为{ra}\nRating排名在{top}\n平均rating为{avg}\n你已经超越了{topp}的玩家。' 245 | return msg 246 | 247 | 248 | async def query_user_top_result(userOrQq: str, mode: str): 249 | if mode == 'qq': 250 | payload = {'qq': userOrQq} 251 | username = await getUsername(payload) 252 | if username not in [400, 403, -1]: 253 | # print(username) 254 | return await getLowMsg(username, 0) 255 | elif username == 400: 256 | return '未找到此QQ' 257 | elif username == 403: 258 | return '此ID不允许查询' 259 | else: 260 | return '未知错误' 261 | else: 262 | return await getLowMsg(userOrQq.lower(), 1) -------------------------------------------------------------------------------- /src/plugins/xray_plugin_utils/__init__.py: -------------------------------------------------------------------------------- 1 | from src.libraries.GLOBAL_PATH import HELP_PATH 2 | from nonebot import on_command 3 | from nonebot.adapters.onebot.v11 import MessageSegment, Message, GroupMessageEvent,Bot 4 | from nonebot.params import CommandArg 5 | 6 | bot_help = on_command('help',aliases={'帮助','菜单','功能'},priority=20) 7 | bot_ping = on_command("ping",priority=20) 8 | 9 | 10 | @bot_ping.handle() 11 | async def _(event: GroupMessageEvent): 12 | await bot_ping.send(f'Pong!') 13 | 14 | @bot_help.handle() 15 | async def _(event: GroupMessageEvent): 16 | with open(HELP_PATH, mode="rb") as f: 17 | data = f.read() 18 | await bot_help.send([MessageSegment.image(data)]) -------------------------------------------------------------------------------- /src/plugins/xray_plugins_generate_completion_status_table/__init__.py: -------------------------------------------------------------------------------- 1 | from nonebot import get_driver 2 | from nonebot.plugin import PluginMetadata 3 | 4 | from . import common 5 | 6 | global_config = get_driver().config 7 | 8 | 9 | __plugin_meta__ = PluginMetadata( 10 | name="MaiMaiDx更新完成表", 11 | description="MaiMaiDx更新完成表", 12 | usage="", 13 | type="application", 14 | config=None, 15 | supported_adapters={"~onebot.v11"} 16 | ) -------------------------------------------------------------------------------- /src/plugins/xray_plugins_generate_completion_status_table/common.py: -------------------------------------------------------------------------------- 1 | import nonebot 2 | from nonebot import on_shell_command 3 | from nonebot.adapters.onebot.v11 import GroupMessageEvent,Bot,Message 4 | from nonebot.params import CommandArg,ShellCommandArgs 5 | from nonebot.log import logger 6 | from src.libraries.GLOBAL_CONSTANT import VERSION_MAP 7 | from nonebot.rule import ArgumentParser,Namespace 8 | from src.libraries.maimai.completion_status_table.new_pcst_base_img import generate_full_version 9 | from src.libraries.GLOBAL_RULE import check_is_bot_admin 10 | 11 | 12 | 13 | nomal_switch_parser = ArgumentParser() 14 | nomal_switch_parser.add_argument("version", metavar="version") 15 | nomal_switch_parser.add_argument("-c", "--abstract", action='store_true') 16 | 17 | generate_version_images = on_shell_command("更新版本完成表",rule=check_is_bot_admin, parser=nomal_switch_parser, priority=20) 18 | @generate_version_images.handle() 19 | async def _(bot: Bot, event: GroupMessageEvent, foo: Namespace = ShellCommandArgs()): 20 | if str(foo.version) in VERSION_MAP.keys(): 21 | # try: 22 | mode = "抽象" if foo.abstract else "正常" 23 | 24 | base_img = generate_full_version(foo.version,foo.abstract) 25 | if base_img is not None: 26 | await generate_version_images.send(f"{str(foo.version)}更新成功-{mode}") 27 | else: 28 | await generate_version_images.send(f"{str(foo.version)}更新失败-{mode},未找到版本:{str(foo.version)}") 29 | 30 | 31 | # if generate_version_image(foo.version,foo.abstract): 32 | # await generate_version_images.send(f"{str(foo.version)}更新成功-{mode}") 33 | # else: 34 | # await generate_version_images.send(f"{str(foo.version)}更新失败-{mode}") 35 | # except Exception as e: 36 | # await generate_version_images.finish(str(e)) 37 | else: 38 | await generate_version_images.finish("未找到版本"+str(foo.version)) 39 | 40 | -------------------------------------------------------------------------------- /src/plugins/xray_plugins_log/__init__.py: -------------------------------------------------------------------------------- 1 | from nonebot.log import logger 2 | import sys 3 | from nonebot import get_driver 4 | from nonebot.log import logger 5 | from logging.handlers import TimedRotatingFileHandler 6 | import logging 7 | import logging.handlers 8 | from src.libraries.GLOBAL_PATH import RUNTIME_LOG_PATH,CUSTOM_LOG_PATH 9 | 10 | driver = get_driver() 11 | 12 | def setup_logger(name, log_file, level=logging.DEBUG): 13 | logger = logging.getLogger(name) 14 | logger.setLevel(level) 15 | logger.propagate = False 16 | if logger.hasHandlers(): 17 | logger.handlers.clear() 18 | handler = TimedRotatingFileHandler(log_file, when="midnight", interval=1, backupCount=30,encoding="utf-8") 19 | handler.setLevel(level) 20 | formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S') 21 | handler.setFormatter(formatter) 22 | logger.addHandler(handler) 23 | return logger 24 | 25 | # 设置日志文件的路径 26 | c_logger = setup_logger('c_logger', f'{CUSTOM_LOG_PATH}/c_logger.log') 27 | 28 | @driver.on_startup 29 | async def set_log_format(): 30 | logger.level("DEBUG", color="", icon=f"*️⃣DDDEBUG") 31 | logger.level("INFO", color="", icon=f"ℹ️IIIINFO") 32 | logger.level("SUCCESS", color="", icon=f"✅SUCCESS") 33 | logger.level("WARNING", color="", icon=f"⚠️WARNING") 34 | logger.level("ERROR", color="", icon=f"⭕EEERROR") 35 | 36 | def default_filter(record): 37 | """默认的日志过滤器,根据 `config.log_level` 配置改变日志等级。""" 38 | log_level = record["extra"].get("nonebot_log_level", "INFO") 39 | levelno = logger.level(log_level).no if isinstance(log_level, str) else log_level 40 | return record["level"].no >= levelno 41 | 42 | default_format: str = ( 43 | "{time:YYYY-MM-DD} {time:HH:mm:ss} " 44 | "[{level.icon}] " 45 | "<{name}.{module}.{function}> " 46 | "{message}") 47 | """默认日志格式""" 48 | 49 | logger.remove() 50 | logger.add( 51 | sys.stdout, 52 | level=0, 53 | diagnose=False, 54 | filter=default_filter, 55 | format=default_format, 56 | ) 57 | 58 | log_file_path = F"{RUNTIME_LOG_PATH}/xray_bot_runtime.log" 59 | 60 | logger.add( 61 | log_file_path, 62 | level=0, 63 | diagnose=False, 64 | filter=default_filter, 65 | format=default_format, 66 | rotation="1 hour" 67 | ) 68 | 69 | logger.success("Log配置项设置完毕") 70 | 71 | -------------------------------------------------------------------------------- /src/plugins/xray_plugins_saveimage/__init__.py: -------------------------------------------------------------------------------- 1 | import string 2 | import random 3 | import httpx 4 | import aiofiles 5 | from nonebot import on_command 6 | from nonebot.adapters.onebot.v11 import GroupMessageEvent, Message 7 | from nonebot.typing import T_State 8 | from typing import Optional 9 | from nonebot.log import logger 10 | from pathlib import Path 11 | from src.libraries.GLOBAL_PATH import DRAGON_PATH 12 | from src.libraries.GLOBAL_CONSTANT import SAVE_IMAGE 13 | 14 | save_along = on_command('添加龙图', priority=20) 15 | 16 | 17 | @save_along.handle() 18 | async def _(event: GroupMessageEvent, state: T_State): 19 | if event.group_id not in SAVE_IMAGE: 20 | await save_along.finish('权限不足') 21 | 22 | @save_along.got("img", prompt="请发送龙图") 23 | async def _(event: GroupMessageEvent, state: T_State): 24 | data = string.ascii_letters + string.digits 25 | filename = "".join(random.sample(data, random.randint(5, 8))) 26 | result = await save_along_img(state["img"], filename) # 保存回答中的图片 27 | await save_along.send(result) 28 | 29 | 30 | 31 | async def save_along_img(msg: Message, file_name:str): 32 | along_path = Path(f"{DRAGON_PATH}/").absolute() 33 | for msg_seg in msg: 34 | if msg_seg.type == "image": 35 | along_file_name = file_name 36 | 37 | file_path = along_path / along_file_name 38 | url = msg_seg.data.get("url", "") 39 | if not url: 40 | continue 41 | data = await get_img(url) 42 | if not data: 43 | continue 44 | result = await save_img(data, file_path) 45 | return result 46 | return "添加失败:消息内容中未检索到图片内容。" 47 | 48 | async def save_img(img: bytes, filepath: Path): 49 | try: 50 | async with aiofiles.open(str(filepath.absolute()), "wb") as f: 51 | await f.write(img) 52 | return "添加成功。" 53 | except: 54 | return "添加失败:图片保存失败,请重试。" 55 | 56 | async def get_img(url: str) -> Optional[bytes]: 57 | headers = { 58 | "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", 59 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36 Edg/95.0.1020.53", 60 | } 61 | try: 62 | async with httpx.AsyncClient() as client: 63 | resp = await client.get(url, headers=headers) 64 | return resp.content 65 | except: 66 | logger.warning(f"图片下载失败:{url}") 67 | return None -------------------------------------------------------------------------------- /src/plugins/xray_plugins_userdata/__init__.py: -------------------------------------------------------------------------------- 1 | # from nonebot.adapters.onebot.v11 import Message 2 | # from nonebot.adapters.onebot.v11.event import GroupMessageEvent 3 | # from nonebot.params import CommandArg 4 | # from pathlib import Path 5 | # from nonebot.plugin import on_command 6 | # from src.libraries.data_handle.userdata_handle import userdata 7 | # from nonebot.log import logger 8 | # from src.libraries.GLOBAL_PATH import FRAME_PATH,PLATE_PATH,CUSTOM_PLATE_PATH,ICON_PATH 9 | 10 | # set_style = on_command("设置主题", priority=20) 11 | # set_abstract_on = on_command("开启抽象画", priority=20) 12 | # set_abstract_off = on_command("关闭抽象画", priority=20) 13 | # set_plate = on_command("设置姓名框", priority=20) 14 | # set_frame = on_command("设置背景板", priority=20,aliases={'设置背景'}) 15 | # set_icon = on_command("设置头像", priority=20) 16 | # set_best30_mode = on_command("设置模式", priority=20) 17 | 18 | # def is_int(content:str): 19 | # try: 20 | # content = int(content) 21 | # return True 22 | # except: 23 | # return False 24 | 25 | # @set_style.handle() 26 | # async def _(event: GroupMessageEvent, args: Message = CommandArg()): 27 | # group_id = event.group_id 28 | # user_id = event.get_user_id() 29 | # style_arg = str(args).strip() 30 | # if style_arg in ["1","2","3"]: 31 | # if userdata.setUserConfig(user_id,group_id,"style_index",style_arg): 32 | # await set_style.send("Best50主题设置成功。") 33 | # else: 34 | # await set_style.send("Best50主题设置出现错误,请联系Xray Bot管理员。") 35 | # else: 36 | # await set_style.send("主题样式目前只有【1】、【2】、【3】请检查主题ID正确。") 37 | 38 | # @set_abstract_on.handle() 39 | # async def _(event: GroupMessageEvent): 40 | # group_id = event.group_id 41 | # user_id = event.get_user_id() 42 | # if userdata.setUserConfig(user_id,group_id,"is_abstract",True): 43 | # await set_abstract_on.send("已开启抽象画模式。") 44 | # else: 45 | # await set_abstract_on.send("抽象画模式设置出现错误,请联系Xray Bot管理员。") 46 | 47 | # @set_abstract_off.handle() 48 | # async def _(event: GroupMessageEvent): 49 | # group_id = event.group_id 50 | # user_id = event.get_user_id() 51 | # if userdata.setUserConfig(user_id,group_id,"is_abstract",False): 52 | # await set_abstract_off.send("已关闭抽象画模式。") 53 | # else: 54 | # await set_abstract_off.send("抽象画模式设置出现错误,请联系Xray Bot管理员。") 55 | 56 | # @set_plate.handle() 57 | # async def _(event: GroupMessageEvent, args: Message = CommandArg()): 58 | # user_id = event.get_user_id() 59 | # plate_id = str(args).strip() 60 | # group_id = event.group_id 61 | # if plate_id == "": 62 | # userdata.setUserConfig(user_id,group_id,"plate","") 63 | # await set_plate.finish("姓名框清除成功。") 64 | 65 | # plate_id = plate_id.zfill(6) if is_int(plate_id) else plate_id 66 | 67 | # path = PLATE_PATH + f"/UI_Plate_{plate_id}.png" 68 | # custom_plate_path = CUSTOM_PLATE_PATH + f"/UI_Plate_{plate_id}.png" 69 | # File = Path(path) 70 | # custom_file = Path(custom_plate_path) 71 | # logger.success(path) 72 | # logger.success(custom_plate_path) 73 | # if File.exists() and File.is_file(): 74 | # if userdata.setUserConfig(user_id,group_id,"plate",plate_id): 75 | # await set_plate.send("姓名框设置成功。") 76 | # else: 77 | # await set_plate.send("姓名框设置出现错误,请联系Xray Bot管理员。") 78 | # elif custom_file.exists() and custom_file.is_file(): 79 | # if userdata.setUserConfig(user_id,group_id,"plate",f"custom_plate{plate_id}"): 80 | # await set_plate.send("姓名框设置成功。") 81 | # else: 82 | # await set_plate.send("姓名框设置出现错误,请联系Xray Bot管理员。") 83 | # else: 84 | # await set_plate.send("姓名框ID错误,请检查ID后重新设置。") 85 | 86 | # @set_frame.handle() 87 | # async def _(event: GroupMessageEvent, args: Message = CommandArg()): 88 | # user_id = event.get_user_id() 89 | # frame_id = str(args).strip() 90 | # group_id = event.group_id 91 | 92 | # frame_id = frame_id.zfill(6) 93 | 94 | # path = FRAME_PATH + f"/UI_Frame_{frame_id}.png" 95 | # File = Path(path) 96 | # if File.exists() and File.is_file(): 97 | # if userdata.setUserConfig(user_id,group_id,"frame",frame_id): 98 | # await set_frame.send("背景设置成功。") 99 | # else: 100 | # await set_frame.send("背景设置出现错误,请联系Xray Bot管理员。") 101 | # else: 102 | # await set_frame.send("背景ID错误,请检查ID后重新设置。") 103 | 104 | # @set_icon.handle() 105 | # async def _(event: GroupMessageEvent, args: Message = CommandArg()): 106 | # user_id = event.get_user_id() 107 | # icon_id = str(args).strip() 108 | # group_id = event.group_id 109 | 110 | # icon_id = icon_id.zfill(6) 111 | 112 | 113 | # path = ICON_PATH + f"/UI_Icon_{icon_id}.png" 114 | # File = Path(path) 115 | # if File.exists() and File.is_file(): 116 | # if userdata.setUserConfig(user_id,group_id,'icon',icon_id): 117 | # await set_icon.send("头像设置成功。") 118 | # else: 119 | # await set_icon.send("头像设置出现错误,请联系Xray Bot管理员。") 120 | # else: 121 | # await set_icon.send("头像ID错误,请检查ID后重新设置。") 122 | 123 | 124 | # @set_best30_mode.handle() 125 | # async def _(event: GroupMessageEvent, args: Message = CommandArg()): 126 | # group_id = event.group_id 127 | # user_id = event.get_user_id() 128 | # style_arg = str(args).strip() 129 | # if style_arg in ["水鱼","落雪"]: 130 | # if userdata.setUserConfig(user_id,group_id,"best30_mode",style_arg): 131 | # await set_best30_mode.send("Best30模式设置成功。") 132 | # else: 133 | # await set_best30_mode.send("Best30模式设置出现错误,请联系Xray Bot管理员。") 134 | # else: 135 | # await set_best30_mode.send("Best30模式样式目前只有【落雪】、【水鱼】请检查主题ID正确。") 136 | -------------------------------------------------------------------------------- /重构命名规范.md: -------------------------------------------------------------------------------- 1 | ### 所有相应事件命名 如 xray_mai_bot 不使用大小驼峰命名法 2 | 3 | ## 事件分类 4 | 5 | #### 如删除别名添加别名 属于操作事件 6 | 7 | ##### 如b50 info 属于用户事件 8 | 9 | 请求方法封装 有多个事件 需要拉取数据 所以一个事件就写了一个拉取方法 出现重复拉取方法。 10 | 11 | lib文件整理 数据库类操作创建移动至对应文件夹 12 | 13 | static文件夹整理 将不同类型资源整理归类 14 | 15 | ##### 将插件内定义的常量迁移至libraries下的全局常量文件中 16 | --------------------------------------------------------------------------------