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
}
}
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:
await self.data.item.put_favorite(user_id, kind_id, fav["itemIdList"])
if "userFavoritemusicList" in upsert and len(upsert["userFavoritemusicList"]) > 0:
for fav in upsert["userFavoritemusicList"]:
await self.data.item.add_fav_music(user_id, fav["id"], fav["orderId"])
# added in BUDDiES+
if "isNewFavoritemusicList" in upsert and upsert["isNewFavoritemusicList"] != "" and "userFavoritemusicList" in upsert:
await self.data.item.put_fav_music(user_id, ((fav["id"], fav["orderId"]) for fav in upsert["userFavoritemusicList"]))
if (
"userFriendSeasonRankingList" in upsert

View File

@ -46,6 +46,9 @@ 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']),
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:
@ -61,11 +64,22 @@ class Mai2Frontend(FE_Base):
if usr_sesh.user_id > 0:
versions = await self.data.profile.get_all_profile_versions(usr_sesh.user_id)
profile = []
new_rival_list = []
if versions:
# maimai_version is -1 means it is not initialized yet, select a default version from existing.
if incoming_ver < 0:
usr_sesh.maimai_version = versions[0]['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]
resp = Response(template.render(
@ -76,7 +90,8 @@ class Mai2Frontend(FE_Base):
profile=profile,
version_list=Mai2Constants.VERSION_STRING,
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")
if incoming_ver < 0:
@ -420,3 +435,28 @@ class Mai2Frontend(FE_Base):
return FileResponse(f"{out_folder}.jpeg")
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 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.engine import Row
from sqlalchemy.schema import ForeignKey
@ -550,25 +551,36 @@ class Mai2ItemData(BaseData):
if result:
return result.fetchall()
async def add_fav_music(self, user_id: int, music_id: int, order_id: Optional[int] = None) -> Optional[int]:
sql = insert(fav_music).values(
user = user_id,
musicId = music_id,
orderId = order_id
)
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(
user = user_id,
musicId = music_id,
orderId = order_id
)
conflict = sql.on_duplicate_key_update(orderId = order_id)
result = await self.execute(conflict)
processed_music_ids.append(music_id)
if not result:
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
conflict = sql.on_duplicate_key_update(orderId = order_id)
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}!")
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:
self.logger.error(f"Failed to remove music {music_id} as favorite for user {user_id}!")
return row_count + result.rowcount
async def put_card(
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:
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)

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>
</th>
</tr>
<tr>
<th>ID:</th>
<th>{{ profile.user }}</th>
</tr>
<tr>
<td>version:</td>
<td>
@ -86,6 +90,40 @@
</tr>
</table>
</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>
{% if error is defined %}
@ -120,6 +158,75 @@
</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>
function changeVersion(sel) {
$.post("/game/mai2/version.change", { version: sel.value })
@ -130,5 +237,17 @@
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>
{% endblock content %}