2026-07-14 22:22:58 +08:00
|
|
|
"""
|
|
|
|
|
This profile module also compatible with official launcher profile format
|
|
|
|
|
"""
|
|
|
|
|
import datetime
|
|
|
|
|
import json
|
|
|
|
|
import logging
|
|
|
|
|
import os
|
|
|
|
|
import uuid
|
|
|
|
|
from copy import deepcopy
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
2026-08-03 00:16:12 +08:00
|
|
|
from PySide6.QtGui import QImage
|
|
|
|
|
|
2026-07-14 22:22:58 +08:00
|
|
|
from core_lib.common.exception import ProfileKeyNotFoundException, ProfileException, ProfileSaveException, \
|
|
|
|
|
ProfileLoadException
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger("Launcher.CoreLib")
|
|
|
|
|
|
|
|
|
|
def get_profile_sample() -> dict:
|
|
|
|
|
return {
|
|
|
|
|
"lastPlayedProfileID": None,
|
|
|
|
|
"profiles": {},
|
|
|
|
|
"settings": {
|
|
|
|
|
# Not all settings are included
|
|
|
|
|
"profileSorting": "ByLastPlayed",
|
|
|
|
|
"showGameLog": True,
|
|
|
|
|
},
|
|
|
|
|
"version": 6
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class Profile:
|
|
|
|
|
def __init__(self, version_id: str, created: datetime.datetime,
|
|
|
|
|
last_used: datetime.datetime | None, name: str="", icon: str | Path="",
|
|
|
|
|
game_dir: str | Path="", java_args: str="", resolution_raw: dict | None = None, type_raw="custom"):
|
|
|
|
|
self.version_id = version_id
|
|
|
|
|
self.created = created
|
|
|
|
|
self.last_used = last_used
|
|
|
|
|
self.name = name
|
|
|
|
|
self.icon: str | Path = icon
|
|
|
|
|
self.game_dir: Path | str = game_dir
|
|
|
|
|
self.java_args: str = java_args
|
|
|
|
|
self.resolution: dict | None = resolution_raw
|
|
|
|
|
self.type: str = type_raw
|
|
|
|
|
|
|
|
|
|
def _get_current_datetime_string() -> str:
|
|
|
|
|
return datetime.datetime.now(datetime.timezone.utc).isoformat().replace("+00:00", "Z")
|
|
|
|
|
|
|
|
|
|
def _create_profile_data(version_id: str, version_type: str, java_args: str= "", game_dir: str= "", icon: str= "", name: str= "", resolution: dict=None) -> dict:
|
|
|
|
|
content = {
|
|
|
|
|
"name": name,
|
|
|
|
|
"created": _get_current_datetime_string(),
|
|
|
|
|
"gameDir": game_dir,
|
|
|
|
|
"lastVersionId": version_id,
|
|
|
|
|
"lastUsed": "",
|
|
|
|
|
"type": version_type,
|
|
|
|
|
"javaArgs": java_args,
|
|
|
|
|
"icon": icon,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if isinstance(resolution, dict):
|
|
|
|
|
content["resolution"] = resolution
|
|
|
|
|
|
|
|
|
|
return content
|
|
|
|
|
|
|
|
|
|
def create_profile_data_from_object(p: Profile):
|
|
|
|
|
created: datetime.datetime | str = p.created
|
|
|
|
|
last_used: datetime.datetime | str | None = p.last_used
|
|
|
|
|
|
|
|
|
|
if isinstance(created, datetime.datetime):
|
|
|
|
|
created = created.isoformat().replace("+00:00", "Z")
|
|
|
|
|
elif not isinstance(created, str):
|
|
|
|
|
raise TypeError("\"created\" key must be a datetime.datetime or str")
|
|
|
|
|
|
|
|
|
|
game_dir: str | Path = p.game_dir
|
|
|
|
|
|
|
|
|
|
if isinstance(game_dir, Path):
|
|
|
|
|
game_dir = game_dir.as_posix()
|
|
|
|
|
|
|
|
|
|
icon: str | Path = p.icon
|
|
|
|
|
|
|
|
|
|
if isinstance(icon, Path):
|
|
|
|
|
icon = icon.as_posix()
|
|
|
|
|
|
|
|
|
|
content = {
|
|
|
|
|
"name": p.name,
|
|
|
|
|
"created": created,
|
|
|
|
|
"gameDir": game_dir,
|
|
|
|
|
"lastVersionId": p.version_id,
|
|
|
|
|
"type": p.type,
|
|
|
|
|
"javaArgs": p.java_args,
|
|
|
|
|
"icon": icon,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if isinstance(last_used, datetime.datetime):
|
|
|
|
|
last_used = last_used.isoformat().replace("+00:00", "Z")
|
|
|
|
|
elif isinstance(last_used, str):
|
|
|
|
|
pass
|
|
|
|
|
elif last_used is not None:
|
|
|
|
|
raise TypeError("\"last_used\" key must be a datetime.datetime or str")
|
|
|
|
|
|
|
|
|
|
if last_used:
|
|
|
|
|
content["lastUsed"] = last_used
|
|
|
|
|
|
|
|
|
|
if isinstance(p.resolution, dict):
|
|
|
|
|
content["resolution"] = p.resolution
|
|
|
|
|
|
|
|
|
|
return content
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def create_profile(version_id: str, version_type: str, java_args: str="", game_dir: str="",
|
|
|
|
|
icon: str="", name: str="", resolution: dict=None, profiles: dict | None=None,
|
|
|
|
|
max_generate_retry=10) -> tuple[dict, str]:
|
|
|
|
|
"""
|
|
|
|
|
Create a new profile with the given arguments
|
|
|
|
|
:param version_id:
|
|
|
|
|
:param version_type:
|
|
|
|
|
:param java_args:
|
|
|
|
|
:param game_dir:
|
|
|
|
|
:param icon:
|
|
|
|
|
:param name:
|
|
|
|
|
:param resolution:
|
|
|
|
|
:param profiles: If not given, this function will create a new profiles dict instead
|
|
|
|
|
:param max_generate_retry:
|
|
|
|
|
:return:
|
|
|
|
|
profile: dict
|
|
|
|
|
new_profile_id: str
|
|
|
|
|
"""
|
|
|
|
|
if profiles is None:
|
|
|
|
|
profiles = get_profile_sample()
|
|
|
|
|
|
|
|
|
|
profile = _create_profile_data(version_id, version_type, java_args, game_dir, icon, name, resolution)
|
|
|
|
|
|
|
|
|
|
for _ in range(max_generate_retry):
|
|
|
|
|
profile_id = str(uuid.uuid4().hex)
|
|
|
|
|
|
|
|
|
|
if profile_id not in profiles["profiles"]:
|
|
|
|
|
break
|
|
|
|
|
else:
|
|
|
|
|
raise ProfileException(
|
|
|
|
|
"Unable to generate a unique profile ID."
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
profiles["profiles"][profile_id] = profile
|
|
|
|
|
|
|
|
|
|
return profile, profile_id
|
|
|
|
|
|
2026-07-29 00:03:25 +08:00
|
|
|
def create_or_save_profile_file(profiles: dict, profile_filepath: Path, profile_file_bak_filepath: Path | None=None,
|
2026-07-14 22:22:58 +08:00
|
|
|
overwrite=True, create_backup=True) -> None:
|
|
|
|
|
"""
|
2026-07-29 00:03:25 +08:00
|
|
|
Create or save a new profile
|
2026-07-14 22:22:58 +08:00
|
|
|
:param profiles:
|
|
|
|
|
:param profile_filepath:
|
|
|
|
|
:param profile_file_bak_filepath:
|
|
|
|
|
:param overwrite:
|
|
|
|
|
:param create_backup:
|
|
|
|
|
:return:
|
|
|
|
|
"""
|
|
|
|
|
profile_filepath.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
|
|
|
|
if profile_filepath.exists() and not overwrite:
|
|
|
|
|
raise ProfileSaveException(
|
|
|
|
|
f"Profile file already exists: {profile_filepath}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Create backup path
|
|
|
|
|
if create_backup and profile_file_bak_filepath is None:
|
|
|
|
|
profile_file_bak_filepath = profile_filepath.with_suffix(
|
|
|
|
|
profile_filepath.suffix + ".bak"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
temporary_path = profile_filepath.with_suffix(
|
|
|
|
|
profile_filepath.suffix + ".tmp"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
# Write temp profile file first
|
|
|
|
|
temporary_path.write_text(
|
|
|
|
|
json.dumps(profiles, indent=4, ensure_ascii=False),
|
|
|
|
|
encoding="utf-8",
|
|
|
|
|
)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
raise ProfileSaveException(
|
|
|
|
|
"Unable to create profile file: {}".format(e)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
if create_backup and profile_filepath.exists():
|
|
|
|
|
if profile_file_bak_filepath.exists():
|
|
|
|
|
profile_file_bak_filepath.unlink()
|
|
|
|
|
|
|
|
|
|
# Rename old profile with an ".bak" extension
|
|
|
|
|
os.replace(
|
|
|
|
|
profile_filepath,
|
|
|
|
|
profile_file_bak_filepath,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Rename new profile file
|
|
|
|
|
os.replace(temporary_path, profile_filepath)
|
|
|
|
|
except OSError as exc:
|
|
|
|
|
if (create_backup and profile_file_bak_filepath is not None and profile_file_bak_filepath.exists()
|
|
|
|
|
and not profile_filepath.exists()):
|
|
|
|
|
os.replace(
|
|
|
|
|
profile_file_bak_filepath,
|
|
|
|
|
profile_filepath,
|
|
|
|
|
)
|
|
|
|
|
raise ProfileSaveException(
|
|
|
|
|
f"Unable to save profile file: {exc}"
|
|
|
|
|
) from exc
|
|
|
|
|
except Exception as e:
|
|
|
|
|
raise ProfileSaveException(
|
|
|
|
|
"Unable to replace path from old to new profile: {}".format(e)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def read_profile_file(profile_filepath: Path) -> dict:
|
|
|
|
|
"""
|
|
|
|
|
Read profile from existing file
|
|
|
|
|
:param profile_filepath:
|
|
|
|
|
:return:
|
|
|
|
|
profiles: dict
|
|
|
|
|
"""
|
|
|
|
|
if not profile_filepath.exists():
|
|
|
|
|
raise ProfileException(
|
|
|
|
|
"Profile file does not exist: {}".format(profile_filepath)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
with open(str(profile_filepath), "r", encoding="utf-8") as f:
|
|
|
|
|
return json.loads(f.read())
|
|
|
|
|
except Exception as e:
|
|
|
|
|
raise ProfileLoadException(
|
|
|
|
|
"Unable to read profile file: {}".format(e)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def _check_profile(profile: dict, fix_wrong=False) -> bool:
|
|
|
|
|
if not isinstance(profile, dict):
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
profile_name = profile.get("name")
|
|
|
|
|
last_version_id = profile.get("lastVersionId", None)
|
|
|
|
|
|
|
|
|
|
if not last_version_id or not isinstance(last_version_id, str):
|
|
|
|
|
return False
|
|
|
|
|
elif profile_name is None or not isinstance(profile_name, str):
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
resolution = profile.get("resolution", None)
|
|
|
|
|
|
|
|
|
|
if resolution is None:
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
if not isinstance(resolution, dict):
|
|
|
|
|
if fix_wrong:
|
|
|
|
|
profile.pop("resolution", None)
|
|
|
|
|
return True
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
width = resolution.get("width")
|
|
|
|
|
height = resolution.get("height")
|
|
|
|
|
|
|
|
|
|
if not isinstance(width, int) or isinstance(width, bool) or width <= 0:
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
if not isinstance(height, int) or isinstance(height, bool) or height <= 0:
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
def is_profiles_valid(profiles: dict, fix_wrong=False, ignore_invalid_profile=False) -> bool:
|
|
|
|
|
"""
|
|
|
|
|
Check if a profiles dictionary is valid
|
|
|
|
|
:param profiles:
|
|
|
|
|
:param fix_wrong:
|
|
|
|
|
:param ignore_invalid_profile:
|
|
|
|
|
:return:
|
|
|
|
|
filtered_profiles: dict
|
|
|
|
|
"""
|
|
|
|
|
if not isinstance(profiles, dict):
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
profiles_map = profiles.get("profiles", None)
|
|
|
|
|
settings = profiles.get("settings", None)
|
|
|
|
|
|
|
|
|
|
if not isinstance(profiles_map, dict):
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
if not isinstance(settings, dict):
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
invalid_ids = []
|
|
|
|
|
|
|
|
|
|
for profile_id, profile in profiles_map.items():
|
|
|
|
|
if not _check_profile(profile, fix_wrong=fix_wrong):
|
|
|
|
|
invalid_ids.append(profile_id)
|
|
|
|
|
|
|
|
|
|
if ignore_invalid_profile:
|
|
|
|
|
for profile_id in invalid_ids:
|
|
|
|
|
del profiles_map[profile_id]
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
return not invalid_ids
|
|
|
|
|
|
2026-07-29 00:03:25 +08:00
|
|
|
def is_profile_valid(profile_data: dict, fix_wrong=False):
|
|
|
|
|
return _check_profile(profile_data, fix_wrong=fix_wrong)
|
|
|
|
|
|
|
|
|
|
def is_profile_exists(profiles: dict, profile_id: str) -> bool:
|
|
|
|
|
if not isinstance(profiles, dict):
|
|
|
|
|
logger.warning("Profile type is not valid: {}".format(type(profiles)))
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
profiles_map: dict = profiles.get("profiles")
|
|
|
|
|
|
|
|
|
|
if not isinstance(profiles_map, dict):
|
|
|
|
|
logger.warning("Profile map type is not valid: {}".format(type(profiles_map)))
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
return profile_id in profiles_map
|
|
|
|
|
|
2026-07-14 22:22:58 +08:00
|
|
|
def list_profiles(profiles: dict) -> dict:
|
|
|
|
|
"""
|
|
|
|
|
List all profiles that inside profiles data
|
|
|
|
|
:param profiles:
|
|
|
|
|
:return:
|
|
|
|
|
"""
|
|
|
|
|
return profiles.get("profiles", {})
|
|
|
|
|
|
|
|
|
|
def get_profile(profiles: dict, profile_id: str) -> dict:
|
|
|
|
|
profile_map = list_profiles(profiles)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
return profile_map[profile_id]
|
|
|
|
|
except KeyError as exc:
|
|
|
|
|
raise ProfileKeyNotFoundException(
|
|
|
|
|
f"Specified profile id {profile_id} not found."
|
|
|
|
|
) from exc
|
|
|
|
|
|
2026-07-29 00:03:25 +08:00
|
|
|
def set_current_profile_id(profiles: dict, profile_id: str):
|
|
|
|
|
"""
|
|
|
|
|
Set current profile id
|
|
|
|
|
INFO: You should ensure profile id is existing in profiles before calling this function.
|
|
|
|
|
:param profiles:
|
|
|
|
|
:param profile_id:
|
|
|
|
|
:return:
|
|
|
|
|
"""
|
|
|
|
|
profiles["lastPlayedProfileID"] = profile_id
|
|
|
|
|
|
|
|
|
|
def get_current_profile_id(profiles: dict) -> str | None:
|
|
|
|
|
return profiles.get("lastPlayedProfileID", None)
|
|
|
|
|
|
|
|
|
|
get_last_played_profile_id = get_current_profile_id
|
|
|
|
|
|
2026-07-14 22:22:58 +08:00
|
|
|
def add_profile(profiles: dict, profile: dict, max_generate_retry=10) -> tuple[dict, str]:
|
|
|
|
|
"""
|
|
|
|
|
Add a new profile
|
|
|
|
|
:param profiles:
|
|
|
|
|
:param profile:
|
|
|
|
|
:param max_generate_retry:
|
|
|
|
|
:return:
|
|
|
|
|
profiles: dict
|
|
|
|
|
new_profile_id: str
|
|
|
|
|
"""
|
|
|
|
|
for _ in range(max_generate_retry):
|
|
|
|
|
profile_id = str(uuid.uuid4().hex)
|
|
|
|
|
|
|
|
|
|
if profile_id not in profiles["profiles"]:
|
|
|
|
|
break
|
|
|
|
|
else:
|
|
|
|
|
raise ProfileException(
|
|
|
|
|
"Unable to generate a unique profile ID."
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
profiles["profiles"][profile_id] = profile
|
|
|
|
|
|
|
|
|
|
return profiles, profile_id
|
|
|
|
|
|
|
|
|
|
def remove_profile(profiles: dict, profile_id: str):
|
|
|
|
|
"""
|
|
|
|
|
Remove a profile
|
|
|
|
|
:param profiles:
|
|
|
|
|
:param profile_id:
|
|
|
|
|
:return:
|
|
|
|
|
"""
|
|
|
|
|
profiles_map = list_profiles(profiles)
|
|
|
|
|
|
|
|
|
|
if not profiles_map:
|
|
|
|
|
raise ProfileException(
|
|
|
|
|
"No profiles exists"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
del profiles_map[profile_id]
|
|
|
|
|
except KeyError:
|
|
|
|
|
raise ProfileKeyNotFoundException("Specified profile id {} not found.".format(profile_id))
|
|
|
|
|
|
2026-07-29 00:03:25 +08:00
|
|
|
def duplicate_profile(profiles: dict, profile_id: str, new_name: str | None=None, max_generate_retry: int=10) -> str:
|
2026-07-14 22:22:58 +08:00
|
|
|
"""
|
|
|
|
|
Duplicate a profile
|
|
|
|
|
:param profiles:
|
|
|
|
|
:param profile_id:
|
2026-07-29 00:03:25 +08:00
|
|
|
:param new_name:
|
2026-07-14 22:22:58 +08:00
|
|
|
:param max_generate_retry:
|
|
|
|
|
:return:
|
|
|
|
|
new_profile_id: str
|
|
|
|
|
"""
|
|
|
|
|
profiles_map = list_profiles(profiles)
|
|
|
|
|
|
|
|
|
|
if not profiles_map:
|
|
|
|
|
raise ProfileException(
|
|
|
|
|
"No profiles exists"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
for _ in range(max_generate_retry):
|
|
|
|
|
new_profile_id = str(uuid.uuid4().hex)
|
|
|
|
|
|
|
|
|
|
if new_profile_id not in profiles_map:
|
|
|
|
|
break
|
|
|
|
|
else:
|
|
|
|
|
raise ProfileException(
|
|
|
|
|
"Unable to generate a unique profile ID."
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
for p_id in profiles_map:
|
|
|
|
|
if p_id == profile_id:
|
2026-07-29 00:03:25 +08:00
|
|
|
new_profile = deepcopy(profiles_map[profile_id])
|
|
|
|
|
|
|
|
|
|
if new_name is not None:
|
|
|
|
|
new_profile["name"] = new_name
|
|
|
|
|
else:
|
|
|
|
|
new_profile["name"] = f'{new_profile.get("name", "")} Copy'
|
|
|
|
|
|
|
|
|
|
profiles["profiles"][new_profile_id] = new_profile
|
2026-07-14 22:22:58 +08:00
|
|
|
return new_profile_id
|
|
|
|
|
|
|
|
|
|
raise ProfileKeyNotFoundException(
|
|
|
|
|
"Specified profile id {} not found.".format(profile_id)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def convert_profile_data_to_object(profiles: dict, profile_id: str) -> Profile:
|
2026-08-03 00:16:12 +08:00
|
|
|
"""
|
|
|
|
|
Convert profiles data to object
|
|
|
|
|
:param profiles:
|
|
|
|
|
:param profile_id:
|
|
|
|
|
:return:
|
|
|
|
|
profiles: dict
|
|
|
|
|
profile_id: str
|
|
|
|
|
"""
|
2026-07-14 22:22:58 +08:00
|
|
|
profile = get_profile(profiles, profile_id)
|
|
|
|
|
|
|
|
|
|
created: str | datetime.datetime = profile.get("created", datetime.datetime.now())
|
|
|
|
|
last_used: None | str | datetime.datetime = profile.get("lastUsed", None)
|
|
|
|
|
|
|
|
|
|
if not isinstance(created, datetime.datetime):
|
|
|
|
|
try:
|
|
|
|
|
created = datetime.datetime.fromisoformat(created)
|
|
|
|
|
except ValueError:
|
|
|
|
|
created = datetime.datetime.now()
|
|
|
|
|
|
|
|
|
|
if isinstance(last_used, str):
|
|
|
|
|
try:
|
|
|
|
|
last_used = datetime.datetime.fromisoformat(last_used)
|
|
|
|
|
except ValueError:
|
|
|
|
|
last_used = None
|
|
|
|
|
|
|
|
|
|
item = Profile(
|
|
|
|
|
version_id=profile.get("lastVersionId", ""),
|
|
|
|
|
created=created,
|
|
|
|
|
last_used=last_used,
|
|
|
|
|
name=profile.get("name", ""),
|
|
|
|
|
icon=profile.get("icon", ""),
|
|
|
|
|
game_dir=profile.get("gameDir", ""),
|
|
|
|
|
java_args=profile.get("javaArgs", ""),
|
|
|
|
|
resolution_raw=profile.get("resolution", {}),
|
|
|
|
|
type_raw=profile.get("type", "custom"),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return item
|
|
|
|
|
|
2026-07-29 00:03:25 +08:00
|
|
|
def update_profile(profiles: dict, profile_id: str, profile_data: dict, fix_wrong: bool=False) -> tuple[dict, str]:
|
|
|
|
|
if not is_profile_exists(profiles, profile_id):
|
|
|
|
|
raise ProfileException(
|
|
|
|
|
"Specified profile id {} not found.".format(profile_id)
|
|
|
|
|
)
|
2026-07-14 22:22:58 +08:00
|
|
|
|
2026-07-29 00:03:25 +08:00
|
|
|
if not is_profile_valid(profile_data, fix_wrong=fix_wrong):
|
|
|
|
|
raise ProfileException(
|
|
|
|
|
"Provided profile data is not valid."
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
profiles["profiles"][profile_id] = profile_data
|
|
|
|
|
|
|
|
|
|
return profiles, profile_id
|
2026-07-14 22:22:58 +08:00
|
|
|
|