Merge branch 'develop' into mai2_enhance_musicscoreapi

This commit is contained in:
2025-08-20 18:28:49 +00:00
6 changed files with 254 additions and 24 deletions

View File

@ -58,3 +58,62 @@ class Mai2BuddiesPlus(Mai2Buddies):
"friendBonusFlag": False "friendBonusFlag": False
} }
} }
async def handle_get_user_friend_check_api_request(self, data: Dict) -> Dict:
user1rivalList = await self.data.profile.get_rivals(data["userId1"])
user2rivalList = await self.data.profile.get_rivals(data["userId2"])
is_user2_in_user1_rivals = any(rival["rival"] == data["userId2"] for rival in user1rivalList)
is_user1_in_user2_rivals = any(rival["rival"] == data["userId1"] for rival in user2rivalList)
if is_user2_in_user1_rivals and is_user1_in_user2_rivals:
return {"returnCode": 0}
else:
return {"returnCode": 1}
async def handle_user_friend_regist_api_request(self, data: Dict) -> Dict:
user1rivalList = await self.data.profile.get_rivals(data["userId1"]) or []
user2rivalList = await self.data.profile.get_rivals(data["userId2"]) or []
is_user2_in_user1_rivals = any(row.rival == data["userId2"] for row in user1rivalList)
is_user1_in_user2_rivals = any(row.rival == data["userId1"] for row in user2rivalList)
user1_show_count = sum(1 for row in user1rivalList if row.show is True)
user2_show_count = sum(1 for row in user2rivalList if row.show is True)
# initialize returnCode
returnCode1 = 2
returnCode2 = 2
# Case1 no rival
if not is_user2_in_user1_rivals and not is_user1_in_user2_rivals:
if user1_show_count >= 3 and user2_show_count >= 3:
returnCode1, returnCode2 = 1, 1
elif user1_show_count >= 3:
returnCode1, returnCode2 = 1, 2
elif user2_show_count >= 3:
returnCode1, returnCode2 = 2, 1
# Case2 has single rival
elif is_user2_in_user1_rivals != is_user1_in_user2_rivals:
if user1_show_count >= 3 and user2_show_count >= 3:
returnCode1, returnCode2 = 1, 1
elif user1_show_count >= 3:
returnCode1, returnCode2 = 1, 2
elif user2_show_count >= 3:
returnCode1, returnCode2 = 2, 1
# execute add_rival and show_rival
if not is_user2_in_user1_rivals:
await self.data.profile.add_rival(data["userId1"], data["userId2"])
if returnCode1 == 2 and user1_show_count < 3:
await self.data.profile.set_rival_shown(data["userId1"], data["userId2"], True)
if not is_user1_in_user2_rivals:
await self.data.profile.add_rival(data["userId2"], data["userId1"])
if returnCode2 == 2 and user2_show_count < 3:
await self.data.profile.set_rival_shown(data["userId2"], data["userId1"], True)
return {
"returnCode1": returnCode1,
"returnCode2": returnCode2
}

View File

@ -258,9 +258,9 @@ class Mai2DX(Mai2Base):
if kind_id is not None: if kind_id is not None:
await self.data.item.put_favorite(user_id, kind_id, fav["itemIdList"]) await self.data.item.put_favorite(user_id, kind_id, fav["itemIdList"])
if "userFavoritemusicList" in upsert and len(upsert["userFavoritemusicList"]) > 0: # added in BUDDiES+
for fav in upsert["userFavoritemusicList"]: if "isNewFavoritemusicList" in upsert and upsert["isNewFavoritemusicList"] != "" and "userFavoritemusicList" in upsert:
await self.data.item.add_fav_music(user_id, fav["id"], fav["orderId"]) await self.data.item.put_fav_music(user_id, ((fav["id"], fav["orderId"]) for fav in upsert["userFavoritemusicList"]))
if ( if (
"userFriendSeasonRankingList" in upsert "userFriendSeasonRankingList" in upsert

View File

@ -46,6 +46,9 @@ class Mai2Frontend(FE_Base):
Route("/update.name", self.update_name, methods=['POST']), Route("/update.name", self.update_name, methods=['POST']),
Route("/version.change", self.version_change, methods=['POST']), Route("/version.change", self.version_change, methods=['POST']),
Route("/photo/{photo_id}", self.get_photo, methods=['GET']), Route("/photo/{photo_id}", self.get_photo, methods=['GET']),
Route("/rival.add", self.rival_POST, methods=['POST']),
Route("/rival.delete", self.rival_POST, methods=['POST']),
Route("/rival.show", self.rival_POST, methods=['POST']),
] ]
async def render_GET(self, request: Request) -> bytes: async def render_GET(self, request: Request) -> bytes:
@ -61,11 +64,22 @@ class Mai2Frontend(FE_Base):
if usr_sesh.user_id > 0: if usr_sesh.user_id > 0:
versions = await self.data.profile.get_all_profile_versions(usr_sesh.user_id) versions = await self.data.profile.get_all_profile_versions(usr_sesh.user_id)
profile = [] profile = []
new_rival_list = []
if versions: if versions:
# maimai_version is -1 means it is not initialized yet, select a default version from existing. # maimai_version is -1 means it is not initialized yet, select a default version from existing.
if incoming_ver < 0: if incoming_ver < 0:
usr_sesh.maimai_version = versions[0]['version'] usr_sesh.maimai_version = versions[0]['version']
profile = await self.data.profile.get_profile_detail(usr_sesh.user_id, usr_sesh.maimai_version) profile = await self.data.profile.get_profile_detail(usr_sesh.user_id, usr_sesh.maimai_version)
rival_list = await self.data.profile.get_rivals(usr_sesh.user_id)
for rival in rival_list:
rivalid = rival["rival"]
rivalShow = rival["show"]
rivalprofile = await self.data.profile.get_profile_detail(rivalid, usr_sesh.maimai_version)
rivalName = rivalprofile["userName"] if rivalprofile else "UnknownName"
rivalRating = rivalprofile["playerRating"] if rivalprofile else 0
new_rival = (rivalName, rivalRating, rivalid, rivalShow)
new_rival_list.append(new_rival)
versions = [x['version'] for x in versions] versions = [x['version'] for x in versions]
resp = Response(template.render( resp = Response(template.render(
@ -76,7 +90,8 @@ class Mai2Frontend(FE_Base):
profile=profile, profile=profile,
version_list=Mai2Constants.VERSION_STRING, version_list=Mai2Constants.VERSION_STRING,
versions=versions, versions=versions,
cur_version=usr_sesh.maimai_version cur_version=usr_sesh.maimai_version,
rival_list=new_rival_list
), media_type="text/html; charset=utf-8") ), media_type="text/html; charset=utf-8")
if incoming_ver < 0: if incoming_ver < 0:
@ -420,3 +435,28 @@ class Mai2Frontend(FE_Base):
return FileResponse(f"{out_folder}.jpeg") return FileResponse(f"{out_folder}.jpeg")
return Response(status_code=404) return Response(status_code=404)
async def rival_POST(self, request: Request):
uri = request.url.path
frm = await request.form()
usr_sesh = self.validate_session(request)
if not usr_sesh:
usr_sesh = UserSession()
if usr_sesh.user_id > 0:
if uri == "/game/mai2/rival.add":
rival_id = frm.get("rivalUserId")
await self.data.profile.add_rival(usr_sesh.user_id, rival_id)
# self.logger.info(f"{usr_sesh.user_id} added a rival")
return RedirectResponse("/game/mai2/", 303)
elif uri == "/game/mai2/rival.delete":
rival_id = frm.get("rivalUserId")
await self.data.profile.remove_rival(usr_sesh.user_id, rival_id)
# self.logger.info(f"{response}")
return RedirectResponse("/game/mai2/", 303)
elif uri == "/game/mai2/rival.show":
rival_id = frm.get("rivalUserId")
show = frm.get("showRival", "false") == "true"
await self.data.profile.set_rival_shown(usr_sesh.user_id, rival_id, show)
return RedirectResponse("/game/mai2/", 303)

View File

@ -1,7 +1,8 @@
from collections.abc import Iterable
from datetime import datetime from datetime import datetime
from typing import Dict, List, Optional from typing import Dict, List, Optional
from sqlalchemy import Column, Table, UniqueConstraint, and_, or_ from sqlalchemy import Column, Table, UniqueConstraint, and_, or_, not_
from sqlalchemy.dialects.mysql import insert from sqlalchemy.dialects.mysql import insert
from sqlalchemy.engine import Row from sqlalchemy.engine import Row
from sqlalchemy.schema import ForeignKey from sqlalchemy.schema import ForeignKey
@ -550,7 +551,11 @@ class Mai2ItemData(BaseData):
if result: if result:
return result.fetchall() return result.fetchall()
async def add_fav_music(self, user_id: int, music_id: int, order_id: Optional[int] = None) -> Optional[int]: async def put_fav_music(self, user_id: int, fav_list: Iterable[tuple[int, Optional[int]]]) -> Optional[int]:
row_count = 0
processed_music_ids = []
for music_id, order_id in fav_list:
sql = insert(fav_music).values( sql = insert(fav_music).values(
user = user_id, user = user_id,
musicId = music_id, musicId = music_id,
@ -558,17 +563,24 @@ class Mai2ItemData(BaseData):
) )
conflict = sql.on_duplicate_key_update(orderId = order_id) conflict = sql.on_duplicate_key_update(orderId = order_id)
result = await self.execute(conflict) result = await self.execute(conflict)
if result:
return result.lastrowid
self.logger.error(f"Failed to add music {music_id} as favorite for user {user_id}!") processed_music_ids.append(music_id)
async def remove_fav_music(self, user_id: int, music_id: int) -> None:
result = await self.execute(fav_music.delete(and_(fav_music.c.user == user_id, fav_music.c.musicId == music_id)))
if not result: if not result:
self.logger.error(f"Failed to remove music {music_id} as favorite for user {user_id}!") self.logger.error(f"Failed to add music {music_id} as favorite for user {user_id}!")
continue
row_count += result.rowcount
clear_stale_entries_stmt = fav_music.delete(and_(fav_music.c.user == user_id, not_(fav_music.c.musicId.in_(processed_music_ids))))
result = await self.execute(clear_stale_entries_stmt)
if result is None:
self.logger.error(f"Failed to clear stale favorite music entries for user {user_id}!")
return None
return row_count + result.rowcount
async def put_card( async def put_card(
self, self,

View File

@ -254,7 +254,7 @@ class Mai2StaticData(BaseData):
async def put_card(self, version: int, card_id: int, card_name: str, opt_id: int = None, **card_data) -> int: async def put_card(self, version: int, card_id: int, card_name: str, opt_id: int = None, **card_data) -> int:
sql = insert(cards).values( sql = insert(cards).values(
version=version, cardId=card_id, cardName=card_name, opt=coalesce(cards.c.opt, opt_id) **card_data version=version, cardId=card_id, cardName=card_name, opt=coalesce(cards.c.opt, opt_id), **card_data
) )
conflict = sql.on_duplicate_key_update(opt=coalesce(cards.c.opt, opt_id), **card_data) conflict = sql.on_duplicate_key_update(opt=coalesce(cards.c.opt, opt_id), **card_data)

View File

@ -17,6 +17,10 @@
<button type="button" class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#name_change">Edit</button> <button type="button" class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#name_change">Edit</button>
</th> </th>
</tr> </tr>
<tr>
<th>ID:</th>
<th>{{ profile.user }}</th>
</tr>
<tr> <tr>
<td>version:</td> <td>version:</td>
<td> <td>
@ -86,6 +90,40 @@
</tr> </tr>
</table> </table>
</div> </div>
<div class="col-lg-8 m-auto mt-3">
<div class="card bg-card rounded">
<table class="table-large table-rowdistinct">
<caption align="top">
RIVALS
<button type="button" class="btn btn-primary btn-sm" style="position: absolute; right: 15px;" data-bs-toggle="modal" data-bs-target="#rival_add">Add</button>
</caption>
<tr>
<th scope="col">Id</th>
<th scope="col">Name</th>
<th scope="col">Rating</th>
<th scope="col">Show</th>
<th scope="col"></th>
</tr>
{% for rival in rival_list %}
<tr>
<td>{{ rival.2 }}</td>
<td>{{ rival.0 }}</td>
<td>{{ rival.1 }}</td>
<td>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch"
id="rivalSwitch_{{ rival.2 }}"
{% if rival.3 %}checked{% endif %}
onchange="submitShow('{{ rival.2 }}', this.checked)">
</div>
</td>
<td>
<button type="button" class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#rival_delete_{{ rival.2 }}">Delete</button>
</td>
</tr>
{% endfor %}
</table>
</div>
</div> </div>
</div> </div>
{% if error is defined %} {% if error is defined %}
@ -120,6 +158,75 @@
</div> </div>
</div> </div>
</div> </div>
<div class="modal fade" id="rival_add" tabindex="-1" aria-labelledby="card_add_label" data-bs-theme="dark" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add rival</h5>
</div>
<div class="modal-body">
<form id="rival" action="/game/mai2/rival.add" method="post" style="outline: 0;">
<label class="form-label" for="rivalUserId">Rival ID</label>
<input class="form-control" aria-describedby="rivalIdHelphelp" form="rival" id="rivalUserId" name="rivalUserId" maxlength="8" type="number" required>
<div id="rivalIdHelphelp" class="form-text">Please use the ID show next to your name in the profile page.</div>
</form>
</div>
<div class="modal-footer">
<input type=submit class="btn btn-primary" type="button" form="rival" value="Add">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
{% for rival in rival_list %}
<div class="modal fade" id="rival_delete_{{ rival.2 }}" tabindex="-1" aria-labelledby="rival_delete_label" data-bs-theme="dark" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Delete rival</h5>
</div>
<div class="modal-body">
<form id="rival_delete_{{ rival.2 }}_form" action="/game/mai2/rival.delete" method="post" style="outline: 0;">
<p>Are you sure you want to delete rival {{ rival.0 }}?</p>
<input type="hidden" name="rivalUserId" value="{{ rival.2 }}">
</form>
</div>
<div class="modal-footer">
<input type="submit" class="btn btn-danger" form="rival_delete_{{ rival.2 }}_form" value="Delete">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
{% endfor %}
{% for rival in rival_list %}
<div class="modal fade" id="rival_show_{{ rival.2 }}" tabindex="-1" aria-labelledby="rival_show_label" data-bs-theme="dark" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{% if rival.3 %}Hide{% else %}Show{% endif %} rival</h5>
</div>
<div class="modal-body">
<form id="rival_show_{{ rival.2 }}_form" action="/game/mai2/rival.show" method="post" style="outline: 0;">
<p>Are you sure you want to {% if rival.3 %}hide{% else %}show{% endif %} rival {{ rival.0 }}?</p>
<input type="hidden" name="rivalUserId" value="{{ rival.2 }}">
<input type="hidden" name="showRival" value="{{ "false" if rival.3 else "true" }}">
</form>
</div>
<div class="modal-footer">
<input type="submit" class="btn btn-primary" form="rival_show_{{ rival.2 }}_form" value="Confirm">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
{% endfor %}
{% for rival in rival_list %}
<form action="/game/mai2/rival.show" method="post" id="rival_show_{{ rival.2 }}_form" style="display: none;">
<input type="hidden" name="rivalUserId" value="{{ rival.2 }}">
<input type="hidden" name="showRival" id="showRival_{{ rival.2 }}" value="false">
</form>
{% endfor %}
<script> <script>
function changeVersion(sel) { function changeVersion(sel) {
$.post("/game/mai2/version.change", { version: sel.value }) $.post("/game/mai2/version.change", { version: sel.value })
@ -130,5 +237,17 @@
alert("Failed to update version."); alert("Failed to update version.");
}); });
} }
function submitShow(rivalId, checked) {
if (checked) {
let shownCount = document.querySelectorAll('.form-check-input:checked').length;
if (shownCount > 3) {
document.getElementById('rivalSwitch_' + rivalId).checked = false;
alert('Show rival limit reached (max 3). Please unselect another rival first.');
return;
}
}
document.getElementById('showRival_' + rivalId).value = checked;
document.getElementById('rival_show_' + rivalId + '_form').submit();
}
</script> </script>
{% endblock content %} {% endblock content %}