Created
June 6, 2026 09:02
-
-
Save yougg/77603db480be1beedbfeb2b21da6dc42 to your computer and use it in GitHub Desktop.
Get Genshin Impact wish history URL running in Wine/Proton on Linux, 获取Linux Wine运行的原神的抽卡记录脚本
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # ============================================================================== | |
| # Script Name: get_gacha_url.sh | |
| # Description: Get Genshin Impact wish history URL running in Wine/Proton on Linux | |
| # Environments: Native Wine, Steam Proton, Lutris, Heroic Games Launcher, AAGL, etc. | |
| # Language: English (Comments) / Adaptive (Console Output) | |
| # ============================================================================== | |
| # Initialize localized messages based on system language | |
| if [[ "$LANG" == *"zh_"* ]]; then | |
| msg_check_curl="错误: 未找到 curl 命令,请先安装 curl。" | |
| msg_finding_log="正在寻找原神日志文件..." | |
| msg_log_not_found="无法找到原神日志文件!请确保您至少在 Wine 中运行过一次游戏并打开过抽卡历史记录。" | |
| msg_log_found="找到日志文件: %s" | |
| msg_wineprefix="定位 WINEPREFIX: %s" | |
| msg_parse_failed="无法在日志中解析出游戏目录!请确认已在游戏中打开过抽卡历史记录页面。" | |
| msg_gamedir="解析游戏目录 (Linux): %s" | |
| msg_gamedir_not_exist="游戏目录不存在,请确认路径解析是否正确: %s" | |
| msg_webcaches_not_found="未找到 webCaches 目录,请在游戏中重新打开一次抽卡历史记录页面!" | |
| msg_cache_ver_not_found="未找到有效的缓存版本目录!" | |
| msg_cache_file_not_found="未找到缓存文件 data_2!" | |
| msg_extracting="正在从缓存文件提取抽卡链接..." | |
| msg_no_links="未能从缓存中找到任何抽卡记录链接!请先在游戏中打开一次抽卡历史记录。" | |
| msg_api_host="使用 API 校验主机名: %s" | |
| msg_verifying="共找到 %d 个缓存链接,正在反向逐一验证有效性..." | |
| msg_success="[成功] 找到有效的抽卡记录链接!" | |
| msg_fail="[错误] 所有的缓存链接均已失效!请在游戏中重新打开抽卡历史记录页面后再试。" | |
| msg_link_invalid="链接 %d 已失效,正在检查更早的链接..." | |
| msg_clipboard_wayland="链接已复制到 Wayland 剪贴板。" | |
| msg_clipboard_x11="链接已复制到 X11 剪贴板。" | |
| msg_clipboard_none="提示: 未检测到 xclip, xsel 或 wl-copy,请手动复制上方链接。" | |
| else | |
| msg_check_curl="Error: curl command not found, please install curl first." | |
| msg_finding_log="Looking for Genshin Impact log file..." | |
| msg_log_not_found="Cannot find Genshin Impact log file! Make sure you have run the game in Wine at least once and opened the Wish History." | |
| msg_log_found="Found log file: %s" | |
| msg_wineprefix="Located WINEPREFIX: %s" | |
| msg_parse_failed="Failed to parse game directory from log! Make sure you have opened the Wish History page in game." | |
| msg_gamedir="Parsed game directory (Linux): %s" | |
| msg_gamedir_not_exist="Game directory does not exist, please check if path mapping is correct: %s" | |
| msg_webcaches_not_found="webCaches directory not found, please reopen the Wish History page in game!" | |
| msg_cache_ver_not_found="Valid cache version directory not found!" | |
| msg_cache_file_not_found="Cache file data_2 not found!" | |
| msg_extracting="Extracting wish history links from cache file..." | |
| msg_no_links="No wish history links found in cache! Please open Wish History in game first." | |
| msg_api_host="Using API verification host: %s" | |
| msg_verifying="Found %d cache links, verifying validity backwards..." | |
| msg_success="[Success] Found valid wish history link!" | |
| msg_fail="[Error] All cache links have expired! Please reopen the Wish History page in game and try again." | |
| msg_link_invalid="Link %d expired, trying the next one..." | |
| msg_clipboard_wayland="Link copied to Wayland clipboard." | |
| msg_clipboard_x11="Link copied to X11 clipboard." | |
| msg_clipboard_none="Note: xclip, xsel or wl-copy not found, please copy the link above manually." | |
| fi | |
| # Check basic dependencies | |
| if ! command -v curl >/dev/null 2>&1; then | |
| echo -e "\033[31m${msg_check_curl}\033[0m" | |
| exit 1 | |
| fi | |
| # Clipboard helper function | |
| copy_to_clipboard() { | |
| local text="$1" | |
| if command -v wl-copy >/dev/null 2>&1; then | |
| echo -n "$text" | wl-copy | |
| echo -e "\033[32m${msg_clipboard_wayland}\033[0m" | |
| elif command -v xclip >/dev/null 2>&1; then | |
| echo -n "$text" | xclip -selection clipboard | |
| echo -e "\033[32m${msg_clipboard_x11}\033[0m" | |
| elif command -v xsel >/dev/null 2>&1; then | |
| echo -n "$text" | xsel --clipboard --input | |
| echo -e "\033[32m${msg_clipboard_x11}\033[0m" | |
| else | |
| echo -e "\033[33m${msg_clipboard_none}\033[0m" | |
| fi | |
| } | |
| # Find Genshin Impact log file output_log.txt | |
| find_log_file() { | |
| # Scan common wineprefix and launcher directories | |
| local common_paths=( | |
| "$HOME/.local/share/anime-game-launcher/prefix/drive_c/users/$USER/AppData/LocalLow/miHoYo" | |
| "$HOME/.wine/drive_c/users/$USER/AppData/LocalLow/miHoYo" | |
| "$HOME/.wine/drive_c/users/steamuser/AppData/LocalLow/miHoYo" | |
| "$HOME/.local/share/an-anime-game-launcher/wineprefix/drive_c/users/steamuser/AppData/LocalLow/miHoYo" | |
| "$HOME/.var/app/com.gitlab.kresiaz.an_anime_game_launcher/data/wineprefix/drive_c/users/steamuser/AppData/LocalLow/miHoYo" | |
| ) | |
| # 1. Look into common directories | |
| for path in "${common_paths[@]}"; do | |
| if [ -d "$path" ]; then | |
| local f | |
| f=$(find "$path" -type f -name "output_log.txt" 2>/dev/null | head -n 1) | |
| if [ -n "$f" ]; then | |
| echo "$f" | |
| return 0 | |
| fi | |
| fi | |
| done | |
| # 2. Search Steam compatdata directory | |
| if [ -d "$HOME/.steam/steam/steamapps/compatdata" ]; then | |
| local f | |
| f=$(find "$HOME/.steam/steam/steamapps/compatdata" -maxdepth 6 -type f -path "*/AppData/LocalLow/miHoYo/*/output_log.txt" 2>/dev/null | head -n 1) | |
| if [ -n "$f" ]; then | |
| echo "$f" | |
| return 0 | |
| fi | |
| fi | |
| # 3. Search Flatpak containers and other user-local folders | |
| local f | |
| f=$(find "$HOME/.var" "$HOME/.local/share" -maxdepth 8 -type f -path "*/AppData/LocalLow/miHoYo/*/output_log.txt" 2>/dev/null | head -n 1) | |
| if [ -n "$f" ]; then | |
| echo "$f" | |
| return 0 | |
| fi | |
| # 4. Fallback: Search User Home directory with limited depth | |
| local f | |
| f=$(find "$HOME" -maxdepth 6 -type f -path "*/AppData/LocalLow/miHoYo/*/output_log.txt" 2>/dev/null | head -n 1) | |
| if [ -n "$f" ]; then | |
| echo "$f" | |
| return 0 | |
| fi | |
| return 1 | |
| } | |
| echo "$msg_finding_log" | |
| log_file=$(find_log_file) | |
| if [ -z "$log_file" ]; then | |
| echo -e "\033[31m${msg_log_not_found}\033[0m" | |
| exit 1 | |
| fi | |
| printf "${msg_log_found}\n" "$log_file" | |
| # Trace WINEPREFIX directory upwards by checking drive_c location | |
| current_dir=$(dirname "$log_file") | |
| wineprefix="" | |
| while [ "$current_dir" != "/" ] && [ -n "$current_dir" ]; do | |
| if [ -d "$current_dir/drive_c" ]; then | |
| wineprefix="$current_dir" | |
| break | |
| fi | |
| current_dir=$(dirname "$current_dir") | |
| done | |
| if [ -z "$wineprefix" ]; then | |
| if [ -n "$WINEPREFIX" ] && [ -d "$WINEPREFIX" ]; then | |
| wineprefix="$WINEPREFIX" | |
| else | |
| wineprefix="$HOME/.wine" | |
| fi | |
| fi | |
| printf "${msg_wineprefix}\n" "$wineprefix" | |
| # Parse game directory Windows path from log | |
| win_path=$(grep -o -E '[A-Za-z]:[/\\][^"'\'']*(GenshinImpact_Data|YuanShen_Data)' "$log_file" | head -n 1 | tr '\\' '/') | |
| if [ -z "$win_path" ]; then | |
| echo -e "\033[31m${msg_parse_failed}\033[0m" | |
| exit 1 | |
| fi | |
| # Extract drive letter and relative path | |
| drive_letter=$(echo "$win_path" | cut -d':' -f1 | tr '[:upper:]' '[:lower:]') | |
| rel_path=$(echo "$win_path" | cut -d':' -f2-) | |
| # Convert Windows path to Linux real path | |
| if [ "$drive_letter" = "c" ]; then | |
| linux_gamedir="${wineprefix}/drive_c${rel_path}" | |
| else | |
| # Check Wine dosdevices mapping | |
| if [ -L "${wineprefix}/dosdevices/${drive_letter}:" ]; then | |
| target_dir=$(readlink -f "${wineprefix}/dosdevices/${drive_letter}:") | |
| linux_gamedir="${target_dir}${rel_path}" | |
| else | |
| # Fallback to drive_c relative append | |
| linux_gamedir="${wineprefix}/drive_c${rel_path}" | |
| fi | |
| fi | |
| printf "${msg_gamedir}\n" "$linux_gamedir" | |
| if [ ! -d "$linux_gamedir" ]; then | |
| printf "\033[31m${msg_gamedir_not_exist}\033[0m\n" "$linux_gamedir" | |
| exit 1 | |
| fi | |
| # Locate webCaches folder (Case-insensitive check for robustness) | |
| webcache_dir="" | |
| if [ -d "$linux_gamedir/webCaches" ]; then | |
| webcache_dir="$linux_gamedir/webCaches" | |
| elif [ -d "$linux_gamedir/webcaches" ]; then | |
| webcache_dir="$linux_gamedir/webcaches" | |
| else | |
| webcache_dir=$(find "$linux_gamedir" -maxdepth 1 -type d -iname "webcaches" | head -n 1) | |
| fi | |
| if [ -z "$webcache_dir" ] || [ ! -d "$webcache_dir" ]; then | |
| echo -e "\033[31m${msg_webcaches_not_found}\033[0m" | |
| exit 1 | |
| fi | |
| # Find the latest modified version directory (ignoring generic cache dirs) | |
| cache_ver_dir=$(find "$webcache_dir" -mindepth 1 -maxdepth 1 -type d | while read -r dir; do | |
| base=$(basename "$dir") | |
| if [[ "$base" =~ ^[0-9]+(\.[0-9]+)+ ]]; then | |
| echo "$(stat -c %Y "$dir" 2>/dev/null || echo 0) $dir" | |
| fi | |
| done | sort -rn | head -n 1 | cut -d' ' -f2-) | |
| if [ -z "$cache_ver_dir" ] || [ ! -d "$cache_ver_dir" ]; then | |
| # Fallback to webcache_dir root if no version subdirectory matches | |
| cache_ver_dir="$webcache_dir" | |
| fi | |
| # Find the cache data file data_2 | |
| cachefile="" | |
| if [ -f "$cache_ver_dir/Cache/Cache_Data/data_2" ]; then | |
| cachefile="$cache_ver_dir/Cache/Cache_Data/data_2" | |
| elif [ -f "$cache_ver_dir/cache/cache_data/data_2" ]; then | |
| cachefile="$cache_ver_dir/cache/cache_data/data_2" | |
| else | |
| cachefile=$(find "$cache_ver_dir" -type f -path "*[Cc]ache/[Cc]ache_[Dd]ata/data_2" 2>/dev/null | head -n 1) | |
| if [ -z "$cachefile" ]; then | |
| cachefile=$(find "$cache_ver_dir" -type f -name "data_2" 2>/dev/null | head -n 1) | |
| fi | |
| fi | |
| if [ -z "$cachefile" ] || [ ! -f "$cachefile" ]; then | |
| echo -e "\033[31m${msg_cache_file_not_found}\033[0m" | |
| exit 1 | |
| fi | |
| echo "$msg_extracting" | |
| tmp_file="/tmp/genshin_gacha_data_2" | |
| cp "$cachefile" "$tmp_file" | |
| # Extract URLs containing webview_gacha | |
| links=() | |
| while read -r line; do | |
| if [ -n "$line" ]; then | |
| links+=("$line") | |
| fi | |
| done < <(grep -o -a -E 'https://[a-zA-Z0-9_./?=&%-]+game_biz=[a-zA-Z0-9_]+' "$tmp_file" | grep "webview_gacha" | uniq) | |
| # Clean up temp file immediately after reading | |
| rm -f "$tmp_file" | |
| if [ ${#links[@]} -eq 0 ]; then | |
| echo -e "\033[31m${msg_no_links}\033[0m" | |
| exit 1 | |
| fi | |
| # Identify default API host based on region | |
| if [[ "$linux_gamedir" == *"YuanShen_Data"* ]]; then | |
| default_api_host="public-operation-hk4e.mihoyo.com" # China Server | |
| else | |
| default_api_host="public-operation-hk4e-sg.hoyoverse.com" # Global Server | |
| fi | |
| # Accept manual override for api host | |
| api_host=${1:-$default_api_host} | |
| if [ "$api_host" = "china" ]; then | |
| api_host="public-operation-hk4e.mihoyo.com" | |
| elif [ "$api_host" = "global" ]; then | |
| api_host="public-operation-hk4e-sg.hoyoverse.com" | |
| fi | |
| printf "${msg_api_host}\n" "$api_host" | |
| printf "${msg_verifying}\n" "${#links[@]}" | |
| wish_history_url="" | |
| link_found=false | |
| # Validate URLs backwards (from newest to oldest) | |
| for ((i=${#links[@]}-1; i>=0; i--)); do | |
| link="${links[i]}" | |
| query_string=$(echo "$link" | cut -d'?' -f2) | |
| cleaned_query=$(echo "$query_string" | sed -E 's/(&|^)(gacha_type|size|lang|limit)=[^&]*//g' | sed 's/^&//') | |
| api_url="https://${api_host}/gacha_info/api/getGachaLog?${cleaned_query}&gacha_type=301&size=5&lang=zh-cn" | |
| response=$(curl -s --max-time 10 "$api_url") | |
| retcode="" | |
| if command -v jq >/dev/null 2>&1; then | |
| retcode=$(echo "$response" | jq -r '.retcode' 2>/dev/null) | |
| fi | |
| if [ -z "$retcode" ]; then | |
| retcode=$(echo "$response" | grep -o '"retcode":[0-9]*' | head -n 1 | cut -d':' -f2) | |
| fi | |
| if [ "$retcode" = "0" ]; then | |
| wish_history_url="$link" | |
| link_found=true | |
| break | |
| fi | |
| printf "${msg_link_invalid}\n" "$(( ${#links[@]} - i ))" | |
| sleep 0.5 | |
| done | |
| if [ "$link_found" = "true" ]; then | |
| echo -e "\n\033[32m${msg_success}\033[0m" | |
| echo "--------------------------------------------------------------------------------" | |
| echo "$wish_history_url" | |
| echo "--------------------------------------------------------------------------------" | |
| copy_to_clipboard "$wish_history_url" | |
| else | |
| echo -e "\n\033[31m${msg_fail}\033[0m" | |
| exit 1 | |
| fi |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Usage example:
运行示例: