From 3843ac6eb14b130cb8819c318c41e110344906ee Mon Sep 17 00:00:00 2001 From: SoulGateKey Date: Thu, 3 Oct 2024 19:32:17 +0000 Subject: [PATCH 01/10] mai2: calc GetGameRanking result --- titles/mai2/base.py | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/titles/mai2/base.py b/titles/mai2/base.py index b041028..9d85857 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -1,3 +1,4 @@ +import pymysql from datetime import datetime, timedelta from typing import Any, Dict, List import logging @@ -76,7 +77,40 @@ class Mai2Base: } async def handle_get_game_ranking_api_request(self, data: Dict) -> Dict: - return {"length": 0, "gameRankingList": []} + conn = pymysql.connect( + host=self.core_config.database.host, + port=self.core_config.database.port, + user=self.core_config.database.username, + password=self.core_config.database.password, + database=self.core_config.database.name, + charset='utf8mb4' + ) + try: + cursor = conn.cursor() + + query = """ + SELECT musicid AS id, COUNT(*) AS point + FROM mai2_playlog + GROUP BY musicid + ORDER BY point DESC + LIMIT 100 + """ + cursor.execute(query) + + results = cursor.fetchall() + ranking_list = [{"id": row[0], "point": row[1], "userName": ""} for row in results] + output = { + "type": 1, + "gameRankingList": ranking_list, + "gameRankingInstantList": None + } + + cursor.close() + conn.close() + return output + + except Exception as e: + return {'length': 0, 'gameRankingList': []} async def handle_get_game_tournament_info_api_request(self, data: Dict) -> Dict: # TODO: Tournament support From 58ae491a8ccd76cae5682b150b2da89bdc3b7295 Mon Sep 17 00:00:00 2001 From: SoulGateKey Date: Thu, 3 Oct 2024 19:47:36 +0000 Subject: [PATCH 02/10] add pymysql to requirements.txt --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index fe5b4ef..72d9844 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,4 +21,5 @@ starlette asyncio uvicorn alembic -python-multipart \ No newline at end of file +python-multipart +pymysql \ No newline at end of file From 0cef797a8a74c6a895fbfaab8035e0bd57a6b65c Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 6 Oct 2024 03:47:10 -0400 Subject: [PATCH 03/10] mai2: rework photo uploads, relates to #67 --- .../versions/d8cd1fa04c2a_mai2_add_photos.py | 38 +++++ titles/mai2/base.py | 61 ++++---- titles/mai2/frontend.py | 130 +++++++++++++++++- titles/mai2/schema/profile.py | 54 +++++++- titles/mai2/templates/mai2_header.jinja | 3 + titles/mai2/templates/mai2_photos.jinja | 28 ++++ 6 files changed, 272 insertions(+), 42 deletions(-) create mode 100644 core/data/alembic/versions/d8cd1fa04c2a_mai2_add_photos.py create mode 100644 titles/mai2/templates/mai2_photos.jinja diff --git a/core/data/alembic/versions/d8cd1fa04c2a_mai2_add_photos.py b/core/data/alembic/versions/d8cd1fa04c2a_mai2_add_photos.py new file mode 100644 index 0000000..312a127 --- /dev/null +++ b/core/data/alembic/versions/d8cd1fa04c2a_mai2_add_photos.py @@ -0,0 +1,38 @@ +"""mai2_add_photos + +Revision ID: d8cd1fa04c2a +Revises: 54a84103b84e +Create Date: 2024-10-06 03:09:15.959817 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = 'd8cd1fa04c2a' +down_revision = '54a84103b84e' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('mai2_user_photo', + sa.Column('id', sa.VARCHAR(length=36), nullable=False), + sa.Column('user', sa.Integer(), nullable=False), + sa.Column('playlog_num', sa.INTEGER(), nullable=False), + sa.Column('track_num', sa.INTEGER(), nullable=False), + sa.Column('when_upload', sa.TIMESTAMP(), server_default=sa.text('now()'), nullable=False), + sa.ForeignKeyConstraint(['user'], ['aime_user.id'], onupdate='cascade', ondelete='cascade'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('user', 'playlog_num', 'track_num', name='mai2_user_photo_uk'), + mysql_charset='utf8mb4' + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('mai2_user_photo') + # ### end Alembic commands ### diff --git a/titles/mai2/base.py b/titles/mai2/base.py index b041028..cb74206 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -2,7 +2,7 @@ from datetime import datetime, timedelta from typing import Any, Dict, List import logging from base64 import b64decode -from os import path, stat, remove +from os import path, stat, remove, mkdir, access, W_OK from PIL import ImageFile from random import randint @@ -866,46 +866,33 @@ class Mai2Base: self.logger.warning(f"Incorrect data size after decoding (Expected 10240, got {len(photo_chunk)})") return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'} - out_name = f"{self.game_config.uploads.photos_dir}/{user_id}_{playlog_id}_{track_num}" + photo_data = await self.data.profile.get_user_photo_by_user_playlog_track(user_id, playlog_id, track_num) + + if not photo_data: + photo_id = await self.data.profile.put_user_photo(user_id, playlog_id, track_num) + else: + photo_id = photo_data['id'] - if not path.exists(f"{out_name}.bin") and div_num != 0: - self.logger.warning(f"Out of order photo upload (div_num {div_num})") - return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'} - - if path.exists(f"{out_name}.bin") and div_num == 0: - self.logger.warning(f"Duplicate file upload") + out_folder = f"{self.game_config.uploads.photos_dir}/{photo_id}" + out_file = f"{out_folder}/{div_num}_{div_len - 1}.bin" + + if not path.exists(out_folder): + mkdir(out_folder) + + if not access(out_folder, W_OK): + self.logger.error(f"Cannot access {out_folder}") return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'} - elif path.exists(f"{out_name}.bin"): - fstats = stat(f"{out_name}.bin") - if fstats.st_size != 10240 * div_num: - self.logger.warning(f"Out of order photo upload (trying to upload div {div_num}, expected div {fstats.st_size / 10240} for file sized {fstats.st_size} bytes)") + if path.exists(out_file): + self.logger.warning(f"Photo chunk {out_file} already exists, skipping") + + else: + with open(out_file, "wb") as f: + written = f.write(photo_chunk) + + if written != len(photo_chunk): + self.logger.error(f"Writing {out_file} failed! Wrote {written} bytes, expected {photo_chunk} bytes") return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'} - - try: - with open(f"{out_name}.bin", "ab") as f: - f.write(photo_chunk) - - except Exception: - self.logger.error(f"Failed writing to {out_name}.bin") - return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'} - - if div_num + 1 == div_len and path.exists(f"{out_name}.bin"): - try: - p = ImageFile.Parser() - with open(f"{out_name}.bin", "rb") as f: - p.feed(f.read()) - - im = p.close() - im.save(f"{out_name}.jpeg") - except Exception: - self.logger.error(f"File {out_name}.bin failed image validation") - - try: - remove(f"{out_name}.bin") - - except Exception: - self.logger.error(f"Failed to delete {out_name}.bin, please remove it manually") return {'returnCode': ret_code, 'apiName': 'UploadUserPhotoApi'} diff --git a/titles/mai2/frontend.py b/titles/mai2/frontend.py index 976e2c4..f1f961a 100644 --- a/titles/mai2/frontend.py +++ b/titles/mai2/frontend.py @@ -1,11 +1,14 @@ from typing import List from starlette.routing import Route, Mount from starlette.requests import Request -from starlette.responses import Response, RedirectResponse -from os import path +from starlette.responses import Response, RedirectResponse, FileResponse +from os import path, walk, remove import yaml import jinja2 -from datetime import datetime +from datetime import datetime, timedelta +from PIL import ImageFile +import re +import shutil from core.frontend import FE_Base, UserSession, PermissionOffset from core.config import CoreConfig @@ -31,7 +34,8 @@ class Mai2Frontend(FE_Base): Route("/", self.render_GET, methods=['GET']), Mount("/playlog", routes=[ Route("/", self.render_GET_playlog, methods=['GET']), - Route("/{index}", self.render_GET_playlog, methods=['GET']), + Route("/{index:int}", self.render_GET_playlog, methods=['GET']), + Route("/photos", self.render_GET_photos, methods=['GET']), ]), Mount("/events", routes=[ Route("/", self.render_events, methods=['GET']), @@ -41,6 +45,7 @@ class Mai2Frontend(FE_Base): ]), Route("/update.name", self.update_name, methods=['POST']), Route("/version.change", self.version_change, methods=['POST']), + Route("/photo/{photo_id}", self.get_photo, methods=['GET']), ] async def render_GET(self, request: Request) -> bytes: @@ -140,6 +145,50 @@ class Mai2Frontend(FE_Base): else: return RedirectResponse("/gate/", 303) + async def render_GET_photos(self, request: Request) -> bytes: + template = self.environment.get_template( + "titles/mai2/templates/mai2_photos.jinja" + ) + usr_sesh = self.validate_session(request) + if not usr_sesh: + usr_sesh = UserSession() + + if usr_sesh.user_id > 0: + if usr_sesh.maimai_version < 0: + return RedirectResponse("/game/mai2/", 303) + + photos = await self.data.profile.get_user_photos_by_user(usr_sesh.user_id) + + photos_fixed = [] + for photo in photos: + if datetime.now().timestamp() > (photo['when_upload'] + timedelta(days=7)).timestamp(): + await self.data.profile.delete_user_photo_by_id(photo['id']) + + if path.exists(f"{self.game_cfg.uploads.photos_dir}/{photo['id']}.jpeg"): + remove(f"{self.game_cfg.uploads.photos_dir}/{photo['id']}.jpeg") + + if path.exists(f"{self.game_cfg.uploads.photos_dir}/{photo['id']}"): + shutil.rmtree(f"{self.game_cfg.uploads.photos_dir}/{photo['id']}") + + continue + + photos_fixed.append({ + "id": photo['id'], + "playlog_num": photo['playlog_num'], + "track_num": photo['track_num'], + "when_upload": photo['when_upload'], + }) + + return Response(template.render( + title=f"{self.core_config.server.name} | {self.nav_name}", + game_list=self.environment.globals["game_list"], + sesh=vars(usr_sesh), + photos=photos_fixed, + expire_days=7, + ), media_type="text/html; charset=utf-8") + else: + return RedirectResponse("/gate/", 303) + async def update_name(self, request: Request) -> bytes: usr_sesh = self.validate_session(request) if not usr_sesh: @@ -299,3 +348,76 @@ class Mai2Frontend(FE_Base): await self.data.static.update_event_by_id(int(event_id), new_enabled, new_start_date) return RedirectResponse("/game/mai2/events/?s=1", 303) + + async def get_photo(self, request: Request) -> RedirectResponse: + usr_sesh = self.validate_session(request) + if not usr_sesh: + return RedirectResponse("/gate/", 303) + + photo_jpeg = request.path_params.get("photo_id", None) + if not photo_jpeg: + return Response(status_code=400) + + matcher = re.match(r"^([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}).jpeg$", photo_jpeg) + if not matcher: + return Response(status_code=400) + + photo_id = matcher.groups()[0] + photo_info = await self.data.profile.get_user_photo_by_id(photo_id) + if not photo_info: + return Response(status_code=404) + + if photo_info["user"] != usr_sesh.user_id: + return Response(status_code=403) + + out_folder = f"{self.game_cfg.uploads.photos_dir}/{photo_id}" + + if datetime.now().timestamp() > (photo_info['when_upload'] + timedelta(days=7)).timestamp(): + await self.data.profile.delete_user_photo_by_id(photo_info['id']) + if path.exists(f"{out_folder}.jpeg"): + remove(f"{out_folder}.jpeg") + + if path.exists(f"{out_folder}"): + shutil.rmtree(out_folder) + + return Response(status_code=404) + + if path.exists(f"{out_folder}"): + print("path exists") + max_idx = 0 + p = ImageFile.Parser() + for _, _, files in walk("out_folder"): + if not files: + break + + matcher = re.match("^(\d+)_(\d+)$", files[0]) + if not matcher: + break + + max_idx = int(matcher.groups()[1]) + + if max_idx + 1 != len(files): + self.logger.error(f"Expected {max_idx + 1} files, found {len(files)}") + max_idx = 0 + break + + if max_idx == 0: + return Response(status_code=500) + + for i in range(max_idx + 1): + with open(f"{out_folder}/{i}_{max_idx}", "rb") as f: + p.feed(f.read()) + try: + im = p.close() + im.save(f"{out_folder}.jpeg") + + except Exception as e: + self.logger.error(f"{photo_id} failed PIL validation! - {e}") + + shutil.rmtree(out_folder) + + if path.exists(f"{out_folder}.jpeg"): + print(f"{out_folder}.jpeg exists") + return FileResponse(f"{out_folder}.jpeg") + + return Response(status_code=404) diff --git a/titles/mai2/schema/profile.py b/titles/mai2/schema/profile.py index 3ff85d2..ede0adf 100644 --- a/titles/mai2/schema/profile.py +++ b/titles/mai2/schema/profile.py @@ -1,9 +1,10 @@ from core.data.schema import BaseData, metadata from titles.mai2.const import Mai2Constants +from uuid import uuid4 from typing import Optional, Dict, List from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_ -from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON, BigInteger, SmallInteger +from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON, BigInteger, SmallInteger, VARCHAR, INTEGER from sqlalchemy.schema import ForeignKey from sqlalchemy.sql import func, select from sqlalchemy.engine import Row @@ -529,6 +530,22 @@ intimacy = Table( mysql_charset="utf8mb4", ) +photo = Table( # end-of-credit memorial photos, NOT user portraits + "mai2_user_photo", + metadata, + Column("id", VARCHAR(36), primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("playlog_num", INTEGER, nullable=False), + Column("track_num", INTEGER, nullable=False), + Column("when_upload", TIMESTAMP, nullable=False, server_default=func.now()), + UniqueConstraint("user", "playlog_num", "track_num", name="mai2_user_photo_uk"), + mysql_charset="utf8mb4", +) + class Mai2ProfileData(BaseData): async def get_all_profile_versions(self, user_id: int) -> Optional[List[Row]]: result = await self.execute(detail.select(detail.c.user == user_id)) @@ -945,6 +962,41 @@ class Mai2ProfileData(BaseData): self.logger.error(f"Failed to update intimacy for user {user_id} and partner {partner_id}!") + async def put_user_photo(self, user_id: int, playlog_num: int, track_num: int) -> Optional[str]: + photo_id = str(uuid4()) + sql = insert(photo).values( + id = photo_id, + user = user_id, + playlog_num = playlog_num, + track_num = track_num, + ) + + conflict = sql.on_duplicate_key_update(user = user_id) + + result = await self.execute(conflict) + if result: + return photo_id + + async def get_user_photo_by_id(self, photo_id: str) -> Optional[Row]: + result = await self.execute(photo.select(photo.c.id.like(photo_id))) + if result: + return result.fetchone() + + async def get_user_photo_by_user_playlog_track(self, user_id: int, playlog_num: int, track_num: int) -> Optional[Row]: + result = await self.execute(photo.select(and_(and_(photo.c.user == user_id, photo.c.playlog_num == playlog_num), photo.c.track_num == track_num))) + if result: + return result.fetchone() + + async def get_user_photos_by_user(self, user_id: int) -> Optional[List[Row]]: + result = await self.execute(photo.select(photo.c.user == user_id)) + if result: + return result.fetchall() + + async def delete_user_photo_by_id(self, photo_id: str) -> Optional[List[Row]]: + result = await self.execute(photo.delete(photo.c.id.like(photo_id))) + if not result: + self.logger.error(f"Failed to delete photo {photo_id}") + async def update_name(self, user_id: int, new_name: str) -> bool: sql = detail.update(detail.c.user == user_id).values( userName=new_name diff --git a/titles/mai2/templates/mai2_header.jinja b/titles/mai2/templates/mai2_header.jinja index f226fbe..7e6757f 100644 --- a/titles/mai2/templates/mai2_header.jinja +++ b/titles/mai2/templates/mai2_header.jinja @@ -3,6 +3,7 @@
  • PROFILE
  • RECORD
  • +
  • PHOTOS
  • {% if sesh is defined and sesh is not none and "{:08b}".format(sesh.permissions)[4] == "1" %}
  • EVENTS
  • {% endif %} @@ -13,6 +14,8 @@ var currentPath = window.location.pathname; if (currentPath === '/game/mai2/') { $('.nav-link[href="/game/mai2/"]').addClass('active'); + } else if (currentPath.startsWith('/game/mai2/playlog/photos')) { + $('.nav-link[href="/game/mai2/playlog/photos"]').addClass('active'); } else if (currentPath.startsWith('/game/mai2/playlog/')) { $('.nav-link[href="/game/mai2/playlog/"]').addClass('active'); } {% if sesh is defined and sesh is not none and "{:08b}".format(sesh.permissions)[4] == "1" %}else if (currentPath.startsWith('/game/mai2/events/')) { diff --git a/titles/mai2/templates/mai2_photos.jinja b/titles/mai2/templates/mai2_photos.jinja new file mode 100644 index 0000000..f112016 --- /dev/null +++ b/titles/mai2/templates/mai2_photos.jinja @@ -0,0 +1,28 @@ +{% extends "core/templates/index.jinja" %} +{% block content %} + +
    + {% include 'titles/mai2/templates/mai2_header.jinja' %} +
    +

    Memorial Photos

    Photos expire after {{ expire_days }} days + {% if photos is defined and photos is not none and photos|length > 0 %} + {% for photo in photos %} +
    + Playlog #{{ photo.playlog_num }} | Track #{{ photo.track_num }} +
    + {{ photo.when_upload }} +
    + +
    +
    + {% endfor %} + {% else %} +
    + No photos +
    + {% endif %} +
    +
    +{% endblock content %} \ No newline at end of file From 451754cf3cde6fe10e622fb2e6a0ea460aee9b89 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 6 Oct 2024 16:09:09 -0400 Subject: [PATCH 04/10] sao: fix my store --- titles/sao/base.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/titles/sao/base.py b/titles/sao/base.py index 44a7801..759d35b 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -688,6 +688,8 @@ class SaoBase: if profile_data['my_shop']: ac = await self.data.arcade.get_arcade(profile_data['my_shop']) if ac: + # TODO: account for machine override + resp.user_basic_data[0].my_store_id = f"{ac['country']}0{ac['id']:04d}" resp.user_basic_data[0].my_store_name = ac['name'] return resp.make() From 033c1aa776b7874b6e534efd664f0b7010271e68 Mon Sep 17 00:00:00 2001 From: SoulGateKey Date: Fri, 11 Oct 2024 16:06:17 +0000 Subject: [PATCH 05/10] =?UTF-8?q?Update=20=E5=8D=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- titles/mai2/base.py | 43 ++++++++++++++++++------------------------- 1 file changed, 18 insertions(+), 25 deletions(-) diff --git a/titles/mai2/base.py b/titles/mai2/base.py index 2d0b686..d498fcf 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -77,40 +77,33 @@ class Mai2Base: } async def handle_get_game_ranking_api_request(self, data: Dict) -> Dict: - conn = pymysql.connect( - host=self.core_config.database.host, - port=self.core_config.database.port, - user=self.core_config.database.username, - password=self.core_config.database.password, - database=self.core_config.database.name, - charset='utf8mb4' - ) try: - cursor = conn.cursor() + playlogs = await self.data.score.get_playlogs(user_id=None) + ranking_list = [] - query = """ - SELECT musicid AS id, COUNT(*) AS point - FROM mai2_playlog - GROUP BY musicid - ORDER BY point DESC - LIMIT 100 - """ - cursor.execute(query) + if not playlogs: + self.logger.warning("No playlogs found.") + return {"length": 0, "gameRankingList": []} - results = cursor.fetchall() - ranking_list = [{"id": row[0], "point": row[1], "userName": ""} for row in results] - output = { + music_count = {} + for log in playlogs: + music_id = log.musicId + music_count[music_id] = music_count.get(music_id, 0) + 1 + + sorted_music = sorted(music_count.items(), key=lambda item: item[1], reverse=True) + + for music_id, count in sorted_music[:100]: + ranking_list.append({"id": music_id, "point": count, "userName": ""}) + + return { "type": 1, "gameRankingList": ranking_list, "gameRankingInstantList": None } - cursor.close() - conn.close() - return output - except Exception as e: - return {'length': 0, 'gameRankingList': []} + self.logger.error(f"Error while getting game ranking: {e}") + return {"length": 0, "gameRankingList": []} async def handle_get_game_tournament_info_api_request(self, data: Dict) -> Dict: # TODO: Tournament support From 29f4a6a696c03170e51a3eb14b0a643f9a2386dc Mon Sep 17 00:00:00 2001 From: SoulGateKey Date: Fri, 11 Oct 2024 16:08:15 +0000 Subject: [PATCH 06/10] revert 033c1aa776b7874b6e534efd664f0b7010271e68 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit revert Update 卖 --- titles/mai2/base.py | 43 +++++++++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/titles/mai2/base.py b/titles/mai2/base.py index d498fcf..2d0b686 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -77,33 +77,40 @@ class Mai2Base: } async def handle_get_game_ranking_api_request(self, data: Dict) -> Dict: + conn = pymysql.connect( + host=self.core_config.database.host, + port=self.core_config.database.port, + user=self.core_config.database.username, + password=self.core_config.database.password, + database=self.core_config.database.name, + charset='utf8mb4' + ) try: - playlogs = await self.data.score.get_playlogs(user_id=None) - ranking_list = [] + cursor = conn.cursor() - if not playlogs: - self.logger.warning("No playlogs found.") - return {"length": 0, "gameRankingList": []} + query = """ + SELECT musicid AS id, COUNT(*) AS point + FROM mai2_playlog + GROUP BY musicid + ORDER BY point DESC + LIMIT 100 + """ + cursor.execute(query) - music_count = {} - for log in playlogs: - music_id = log.musicId - music_count[music_id] = music_count.get(music_id, 0) + 1 - - sorted_music = sorted(music_count.items(), key=lambda item: item[1], reverse=True) - - for music_id, count in sorted_music[:100]: - ranking_list.append({"id": music_id, "point": count, "userName": ""}) - - return { + results = cursor.fetchall() + ranking_list = [{"id": row[0], "point": row[1], "userName": ""} for row in results] + output = { "type": 1, "gameRankingList": ranking_list, "gameRankingInstantList": None } + cursor.close() + conn.close() + return output + except Exception as e: - self.logger.error(f"Error while getting game ranking: {e}") - return {"length": 0, "gameRankingList": []} + return {'length': 0, 'gameRankingList': []} async def handle_get_game_tournament_info_api_request(self, data: Dict) -> Dict: # TODO: Tournament support From 398fa9059d3f14900721678934ad546a76061481 Mon Sep 17 00:00:00 2001 From: SoulGateKey Date: Fri, 11 Oct 2024 16:09:53 +0000 Subject: [PATCH 07/10] Update mai2/base.py using the ORM --- titles/mai2/base.py | 43 ++++++++++++++++++------------------------- 1 file changed, 18 insertions(+), 25 deletions(-) diff --git a/titles/mai2/base.py b/titles/mai2/base.py index 2d0b686..d498fcf 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -77,40 +77,33 @@ class Mai2Base: } async def handle_get_game_ranking_api_request(self, data: Dict) -> Dict: - conn = pymysql.connect( - host=self.core_config.database.host, - port=self.core_config.database.port, - user=self.core_config.database.username, - password=self.core_config.database.password, - database=self.core_config.database.name, - charset='utf8mb4' - ) try: - cursor = conn.cursor() + playlogs = await self.data.score.get_playlogs(user_id=None) + ranking_list = [] - query = """ - SELECT musicid AS id, COUNT(*) AS point - FROM mai2_playlog - GROUP BY musicid - ORDER BY point DESC - LIMIT 100 - """ - cursor.execute(query) + if not playlogs: + self.logger.warning("No playlogs found.") + return {"length": 0, "gameRankingList": []} - results = cursor.fetchall() - ranking_list = [{"id": row[0], "point": row[1], "userName": ""} for row in results] - output = { + music_count = {} + for log in playlogs: + music_id = log.musicId + music_count[music_id] = music_count.get(music_id, 0) + 1 + + sorted_music = sorted(music_count.items(), key=lambda item: item[1], reverse=True) + + for music_id, count in sorted_music[:100]: + ranking_list.append({"id": music_id, "point": count, "userName": ""}) + + return { "type": 1, "gameRankingList": ranking_list, "gameRankingInstantList": None } - cursor.close() - conn.close() - return output - except Exception as e: - return {'length': 0, 'gameRankingList': []} + self.logger.error(f"Error while getting game ranking: {e}") + return {"length": 0, "gameRankingList": []} async def handle_get_game_tournament_info_api_request(self, data: Dict) -> Dict: # TODO: Tournament support From a673d9dabd4cde016a9239ec45b5ffab37246364 Mon Sep 17 00:00:00 2001 From: SoulGateKey Date: Fri, 11 Oct 2024 16:12:53 +0000 Subject: [PATCH 08/10] Delete unused dependency --- titles/mai2/base.py | 1 - 1 file changed, 1 deletion(-) diff --git a/titles/mai2/base.py b/titles/mai2/base.py index d498fcf..cde1e1e 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -1,4 +1,3 @@ -import pymysql from datetime import datetime, timedelta from typing import Any, Dict, List import logging From 598e4aad76dbc915fae47ff2c61b469732c20c9b Mon Sep 17 00:00:00 2001 From: SoulGateKey Date: Fri, 11 Oct 2024 16:16:40 +0000 Subject: [PATCH 09/10] Update mai2/schema/score.py to support new handle_get_game_ranking --- titles/mai2/schema/score.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/titles/mai2/schema/score.py b/titles/mai2/schema/score.py index f62466a..e376216 100644 --- a/titles/mai2/schema/score.py +++ b/titles/mai2/schema/score.py @@ -396,8 +396,11 @@ class Mai2ScoreData(BaseData): return result.fetchall() async def get_playlogs(self, user_id: int, idx: int = 0, limit: int = 0) -> Optional[List[Row]]: - sql = playlog.select(playlog.c.user == user_id) - + if user_id is not None: + sql = playlog.select(playlog.c.user == user_id) + else: + sql = playlog.select() + if limit: sql = sql.limit(limit) if idx: From b6e7e0973b0db16bc0ac1d77657934c7f34f245c Mon Sep 17 00:00:00 2001 From: SoulGateKey Date: Fri, 11 Oct 2024 16:19:07 +0000 Subject: [PATCH 10/10] Delete unused dependency --- requirements.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 72d9844..fe5b4ef 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,5 +21,4 @@ starlette asyncio uvicorn alembic -python-multipart -pymysql \ No newline at end of file +python-multipart \ No newline at end of file