VPS one-click scripts
All scripts / media

media Streaming unlock & IP check

To see which streaming and AI services your VPS IP can use, run the command below. It only uses curl to visit each site and interprets the response, then looks up the IP's ASN and type. Nothing is installed.

Alias of unlock

Command
$ curl -fsSL xyz.sh/media | bash
Last updated:
Declared OS support:Debian, Ubuntu, CentOS, Rocky, AlmaLinux
Aliases:ipcheck media
Root optional
Upstream:https://github.com/lmc999/RegionRestrictionCheck
SHA256:b86e330452726fe8e8f1f7712eac6f9e2dcaebae4de04cbacc2a8366194ed3fd · Matches the script curl receives. You can also fetch /media.sh.sha256
Read the tutorial:VPS Streaming Unlock Check Tutorial (with IP Quality)

Every script passes bash -n and ShellCheck and was tested with curl | bash as root in the official Docker images of Debian 12/11, Ubuntu 24.04/22.04/20.04/18.04, Rocky 9, AlmaLinux 9 and CentOS 7 (see “Container test results” on each page). Containers lack systemd, kernel modules and real public routes, so those parts still need a real VPS.

What it does

A small checker written by xyz.sh that only makes curl requests and installs nothing: Netflix (full / originals only), Disney+, YouTube Premium region, ChatGPT (including whether the app flags the IP as VPN), Claude, Gemini and TikTok region; IP information (ASN, country, datacenter / residential / mobile, VPN / proxy flags) comes from the free public APIs api.ipapi.is and ip-api.com (this sends the server IP to them; skip with --no-ip). These are heuristics based on what each site returns right now and sites change often. For a much larger matrix, --full runs lmc999/RegionRestrictionCheck and --ipquality runs xykt/IPQuality (downloaded, previewed, and executed only after confirmation; attribution and licences are shown).

Supported OS

  • Debian
  • Ubuntu
  • CentOS
  • Rocky
  • AlmaLinux

Root optional

Flags and examples

$ curl -fsSL xyz.sh/unlock | bash
$ curl -fsSL xyz.sh/unlock | bash -s -- -6
$ curl -fsSL xyz.sh/unlock | bash -s -- --json
$ curl -fsSL xyz.sh/unlock | bash -s -- --no-ip
$ curl -fsSL xyz.sh/unlock | bash -s -- --full
$ curl -fsSL xyz.sh/ipcheck | bash -s -- --ipquality

Upstream: https://github.com/lmc999/RegionRestrictionCheck

Container test results

Results of curl | bash as root without a terminal in each distribution's official Docker image. partial means the container could not verify everything (for example no systemd, no kernel module, or the test protocol blocked by the test environment's proxy) and a real VPS is needed to confirm. (2026-11-11)

OSResult
debian:12partial
debian:11partial
ubuntu:24.04partial
ubuntu:22.04partial
ubuntu:20.04partial
ubuntu:18.04partial
rockylinux:9partial
almalinux:9partial
centos:7partial

FAQ

Is this script safe?
xyz.sh adds no telemetry and performs no hidden actions. Read the full source on the page or download it with curl -fsSL xyz.sh/<name>.sh first; every script has a /<name>.sh.sha256 checksum. Piping a remote script to bash is still a risk — review the source first. The IP lookup sends the server IP to api.ipapi.is and ip-api.com (skip with --no-ip). --full and --ipquality run the third-party scripts lmc999/RegionRestrictionCheck and xykt/IPQuality after a preview and confirmation.
Which operating systems are supported?
Declared support: Debian, Ubuntu, CentOS, Rocky Linux, and AlmaLinux. The script detects the distribution and warns on an unknown one. It was tested in Docker containers of these systems (syntax, --dry-run, and whatever can run in a container; see tests/RESULTS.md); parts that need systemd, kernel modules, or the public internet still need a real VPS.
Do I need root?
No. It runs as a normal user; as root some tools can be installed system-wide.
Why do results differ from other scripts?
Sites change often and every unlock checker infers from what a page or API returns at that moment. Time, IPv4 vs IPv6 and DNS-based unlocking all change results. Actual use is the final answer.

Full source

This is exactly what curl receives (common library + script body). Read it before running.

xyz.sh/media.sh
#!/usr/bin/env bash
# Served by xyz.sh/media
# Human-readable page: https://xyz.sh/media
# This file is the common library followed by the script body.
# No telemetry is added by xyz.sh.
# =============================================================================
# xyz.sh common library
# Shared helpers for every script served by xyz.sh.
#
# Promises:
#   - No telemetry, analytics, or background "phone-home" calls
#   - No hidden installs or config changes beyond what the script announces
#   - Official upstream URLs are printed before any remote code is executed
#   - Destructive or system-changing steps ask for confirmation (or YES=1 / -y)
# =============================================================================

if [[ -n "${XYZ_COMMON_LOADED:-}" ]]; then
  # When sourced a second time, return. When executed as a file, return fails.
  # shellcheck disable=SC2317
  return 0 2>/dev/null || true
fi
# shellcheck disable=SC2034
XYZ_COMMON_LOADED=1

set -euo pipefail

XYZ_VERSION="1.3.0"
XYZ_BASE="${XYZ_BASE:-https://xyz.sh}"
XYZ_NAME="${XYZ_NAME:-unknown}"

# --- language ---------------------------------------------------------------
# Override with XYZ_LANG=zh or XYZ_LANG=en.
if [[ -z "${XYZ_LANG:-}" ]]; then
  case "${LC_ALL:-${LC_MESSAGES:-${LANG:-}}}" in
    zh*|ZH*|cmn*|CMN*) XYZ_LANG="zh" ;;
    *) XYZ_LANG="en" ;;
  esac
fi

xyz_t() {
  # xyz_t "English text" "中文文本"
  if [[ "${XYZ_LANG}" == "zh" ]]; then
    printf '%s' "${2:-$1}"
  else
    printf '%s' "$1"
  fi
}

# --- colors (TTY only) ------------------------------------------------------
# XYZ_COLOR=always|never|auto (default auto: TTY and no NO_COLOR); --no-color.
_xyz_color=0
case "${XYZ_COLOR:-auto}" in
  always) _xyz_color=1 ;;
  never) _xyz_color=0 ;;
  *) [[ -t 1 && -z "${NO_COLOR:-}" ]] && _xyz_color=1 ;;
esac
[[ "${XYZ_NO_COLOR:-0}" == "1" ]] && _xyz_color=0
if [[ "${_xyz_color}" == "1" ]]; then
  XYZ_C_RESET=$'\033[0m'
  XYZ_C_BOLD=$'\033[1m'
  XYZ_C_DIM=$'\033[2m'
  XYZ_C_RED=$'\033[31m'
  XYZ_C_GREEN=$'\033[32m'
  XYZ_C_YELLOW=$'\033[33m'
  XYZ_C_CYAN=$'\033[36m'
else
  XYZ_C_RESET=""
  XYZ_C_BOLD=""
  XYZ_C_DIM=""
  XYZ_C_RED=""
  XYZ_C_GREEN=""
  XYZ_C_YELLOW=""
  XYZ_C_CYAN=""
fi

xyz_info()  { printf '%s[INFO]%s %s\n'  "${XYZ_C_GREEN}"  "${XYZ_C_RESET}" "$*"; }
xyz_warn()  { printf '%s[WARN]%s %s\n'  "${XYZ_C_YELLOW}" "${XYZ_C_RESET}" "$*" >&2; }
xyz_error() { printf '%s[ERROR]%s %s\n' "${XYZ_C_RED}"    "${XYZ_C_RESET}" "$*" >&2; }
xyz_step()  { printf '%s==>%s %s\n'     "${XYZ_C_CYAN}"   "${XYZ_C_RESET}" "$*"; }

# --- args / init ------------------------------------------------------------
XYZ_ARGS=()
XYZ_YES=0
XYZ_DRY_RUN=0

xyz_parse_global_args() {
  local filtered=()
  local arg
  for arg in "$@"; do
    case "${arg}" in
      -y|--yes)
        XYZ_YES=1
        ;;
      --lang=zh|zh)
        XYZ_LANG="zh"
        ;;
      --lang=en|en)
        XYZ_LANG="en"
        ;;
      --dry-run)
        XYZ_DRY_RUN=1
        ;;
      --no-color)
        XYZ_NO_COLOR=1
        XYZ_C_RESET=""; XYZ_C_BOLD=""; XYZ_C_DIM=""; XYZ_C_RED=""
        XYZ_C_GREEN=""; XYZ_C_YELLOW=""; XYZ_C_CYAN=""
        ;;
      --help|-h)
        XYZ_HELP=1
        filtered+=("${arg}")
        ;;
      *)
        filtered+=("${arg}")
        ;;
    esac
  done
  XYZ_ARGS=("${filtered[@]+"${filtered[@]}"}")
  if [[ "${YES:-}" =~ ^(1|yes|true|YES)$ || "${XYZ_ASSUME_YES:-}" == "1" ]]; then
    XYZ_YES=1
  fi
  # Exported so individual scripts can read flags set here.
  export XYZ_ARGS XYZ_YES XYZ_LANG XYZ_HELP="${XYZ_HELP:-}" XYZ_DRY_RUN="${XYZ_DRY_RUN:-0}"
  export XYZ_NO_COLOR="${XYZ_NO_COLOR:-0}"
  if declare -F xyz_ui_init >/dev/null; then xyz_ui_init; fi
}

xyz_banner() {
  local title="${XYZ_TITLE:-$XYZ_NAME}"
  printf '\n%s%sxyz.sh/%s%s  v%s\n' "${XYZ_C_BOLD}" "${XYZ_C_CYAN}" "${XYZ_NAME}" "${XYZ_C_RESET}" "${XYZ_VERSION}"
  printf '%s\n' "${XYZ_C_DIM}----------------------------------------${XYZ_C_RESET}"
  printf '  %s: %s/%s\n' "$(xyz_t "Page" "页面")" "${XYZ_BASE}" "${XYZ_NAME}"
  printf '  %s: %s/%s.sh\n' "$(xyz_t "Raw" "源码")" "${XYZ_BASE}" "${XYZ_NAME}"
  if [[ -n "${XYZ_UPSTREAM:-}" ]]; then
    printf '  %s: %s\n' "$(xyz_t "Upstream" "上游来源")" "${XYZ_UPSTREAM}"
  fi
  printf '  %s\n' "$(xyz_t "No telemetry, no hidden actions." "无遥测、无隐藏操作。")"
  printf '%s\n\n' "${XYZ_C_DIM}----------------------------------------${XYZ_C_RESET}"
  if [[ -n "${title}" && "${title}" != "${XYZ_NAME}" ]]; then
    xyz_step "${title}"
  fi
}

xyz_init() {
  xyz_parse_global_args "$@"
  xyz_detect_os
  xyz_banner
}

# --- root / OS --------------------------------------------------------------
xyz_is_root() {
  [[ "${EUID:-$(id -u)}" -eq 0 ]]
}

xyz_require_root() {
  if ! xyz_is_root; then
    xyz_error "$(xyz_t \
      "This script must be run as root. Try: sudo bash" \
      "请使用 root 运行。可尝试:sudo bash")"
    exit 1
  fi
}

xyz_prefer_root() {
  if ! xyz_is_root; then
    xyz_warn "$(xyz_t \
      "Not running as root; some information or tests may be limited." \
      "当前不是 root,部分信息或测试可能不完整。")"
  fi
}

XYZ_OS_ID=""
XYZ_OS_LIKE=""
XYZ_OS_VERSION=""
XYZ_OS_FAMILY=""   # debian | rhel | unknown
XYZ_OS_PRETTY=""

xyz_detect_os() {
  XYZ_OS_ID=""
  XYZ_OS_LIKE=""
  XYZ_OS_VERSION=""
  XYZ_OS_FAMILY="unknown"
  XYZ_OS_PRETTY="unknown"
  if [[ -r /etc/os-release ]]; then
    # shellcheck disable=SC1091
    . /etc/os-release
    XYZ_OS_ID="${ID:-}"
    XYZ_OS_LIKE="${ID_LIKE:-}"
    XYZ_OS_VERSION="${VERSION_ID:-}"
    XYZ_OS_PRETTY="${PRETTY_NAME:-$XYZ_OS_ID}"
    export XYZ_OS_ID XYZ_OS_LIKE XYZ_OS_VERSION XYZ_OS_FAMILY XYZ_OS_PRETTY
  fi
  case "${XYZ_OS_ID}" in
    debian|ubuntu)
      XYZ_OS_FAMILY="debian"
      ;;
    centos|rhel|rocky|almalinux|alma|fedora)
      XYZ_OS_FAMILY="rhel"
      ;;
    *)
      case " ${XYZ_OS_ID} ${XYZ_OS_LIKE} " in
        *debian*|*ubuntu*) XYZ_OS_FAMILY="debian" ;;
        *rhel*|*centos*|*fedora*|*rocky*|*alma*) XYZ_OS_FAMILY="rhel" ;;
        *) : ;;
      esac
      ;;
  esac
}

xyz_os_supported() {
  case "${XYZ_OS_ID}" in
    debian|ubuntu|centos|rocky|almalinux) return 0 ;;
    *) : ;;
  esac
  case " ${XYZ_OS_LIKE} " in
    *debian*|*ubuntu*|*rhel*|*centos*|*rocky*|*alma*) return 0 ;;
    *) : ;;
  esac
  return 1
}

xyz_require_supported_os() {
  xyz_detect_os
  if ! xyz_os_supported; then
    xyz_warn "$(xyz_t \
      "Unrecognized distribution: ${XYZ_OS_ID:-unknown} (${XYZ_OS_PRETTY}). Officially supported: Debian, Ubuntu, CentOS, Rocky, AlmaLinux." \
      "未能识别的发行版:${XYZ_OS_ID:-unknown}(${XYZ_OS_PRETTY})。官方支持:Debian、Ubuntu、CentOS、Rocky、AlmaLinux。")"
    if ! xyz_confirm "$(xyz_t "Continue anyway?" "仍然继续?")"; then
      exit 1
    fi
  else
    xyz_info "$(xyz_t "Detected OS:" "检测到系统:") ${XYZ_OS_PRETTY} (${XYZ_OS_FAMILY})"
  fi
}

# --- packages / commands ----------------------------------------------------
xyz_need_cmd() {
  command -v "$1" >/dev/null 2>&1
}

xyz_pkg_install() {
  xyz_detect_os
  if (( $# == 0 )); then
    return 0
  fi
  case "${XYZ_OS_FAMILY}" in
    debian)
      export DEBIAN_FRONTEND=noninteractive
      apt-get update -y || xyz_warn "$(xyz_t "apt-get update reported errors (a repository may be unreachable); trying to install anyway." "apt-get update 报错(可能有软件源无法访问),仍尝试安装。")"
      apt-get install -y --no-install-recommends "$@"
      ;;
    rhel)
      if xyz_need_cmd dnf; then
        dnf install -y "$@"
      elif xyz_need_cmd yum; then
        yum install -y "$@"
      else
        xyz_error "$(xyz_t "Neither dnf nor yum is available." "找不到 dnf 或 yum。")"
        exit 1
      fi
      ;;
    *)
      xyz_error "$(xyz_t "Cannot install packages on this OS automatically." "无法在此系统上自动安装软件包。")"
      exit 1
      ;;
  esac
}

xyz_ensure_cmds() {
  local pkg
  local missing=()
  for pkg in "$@"; do
    if ! xyz_need_cmd "${pkg}"; then
      missing+=("${pkg}")
    fi
  done
  if ((${#missing[@]} > 0)); then
    xyz_info "$(xyz_t "Installing missing commands:" "正在安装缺失命令:") ${missing[*]}"
    xyz_pkg_install "${missing[@]}"
  fi
}

# --- confirmation (safe with curl | bash) -----------------------------------
# A readable /dev/tty node is not enough: in containers or curl|bash without a
# controlling terminal, open() fails with ENXIO ("No such device or address").
# [[ -r /dev/tty ]] is true in that case and must not be used as the check.
xyz_open_tty() {
  # Opens /dev/tty into XYZ_TTY_FD if it is a real terminal. Caller must close.
  local fd
  XYZ_TTY_FD=""
  # Brace group so a failed open cannot leak "No such device or address".
  if { exec {fd}<>/dev/tty; } 2>/dev/null && [[ -t "${fd}" ]]; then
    XYZ_TTY_FD="${fd}"
    return 0
  fi
  if [[ -n "${fd:-}" ]]; then
    exec {fd}>&- 2>/dev/null || true
  fi
  return 1
}

xyz_close_tty() {
  if [[ -n "${XYZ_TTY_FD:-}" ]]; then
    exec {XYZ_TTY_FD}>&- 2>/dev/null || true
    XYZ_TTY_FD=""
  fi
}

xyz_confirm() {
  local prompt="$1"
  if [[ "${XYZ_YES}" == "1" ]]; then
    xyz_info "$(xyz_t "Auto-confirmed (-y / YES=1):" "已自动确认(-y / YES=1):") ${prompt}"
    return 0
  fi
  local reply=""
  if xyz_open_tty; then
    printf '%s [y/N] ' "${prompt}" >&"${XYZ_TTY_FD}"
    # shellcheck disable=SC2162
    read -r reply <&"${XYZ_TTY_FD}" || true
    xyz_close_tty
  else
    xyz_warn "$(xyz_t \
      "No TTY available for confirmation. Re-run with -y or YES=1 if you accept." \
      "没有可用的终端来确认。若你已了解并同意,请加 -y 或 YES=1 重试。")"
    return 1
  fi
  case "${reply}" in
    y|Y|yes|YES|Yes) return 0 ;;
    *) return 1 ;;
  esac
}

# --- download helpers -------------------------------------------------------
# Create the temp dir in the main shell. xyz_tmpdir is usually called inside
# $(...), a subshell, so creating it (and its EXIT trap) there would delete the
# directory as soon as the subshell returned.
xyz_init_tmpdir() {
  if [[ -z "${XYZ_TMPDIR:-}" || ! -d "${XYZ_TMPDIR}" ]]; then
    XYZ_TMPDIR="$(mktemp -d "${TMPDIR:-/tmp}/xyzsh.XXXXXX")"
    # shellcheck disable=SC2064
    trap "rm -rf '${XYZ_TMPDIR}'" EXIT
  fi
}

xyz_tmpdir() {
  if [[ -z "${XYZ_TMPDIR:-}" || ! -d "${XYZ_TMPDIR}" ]]; then
    xyz_error "internal: temp dir not initialised"
    exit 1
  fi
  printf '%s' "${XYZ_TMPDIR}"
}

xyz_fetch() {
  # xyz_fetch URL DEST
  local url="$1"
  local dest="$2"
  xyz_info "$(xyz_t "Downloading" "正在下载"): ${url}"
  xyz_info "$(xyz_t "Saving to" "保存到"): ${dest}"
  if xyz_need_cmd curl; then
    curl -fsSL --connect-timeout 20 --max-time 180 --retry 2 --retry-delay 1 \
      -o "${dest}" "${url}"
  elif xyz_need_cmd wget; then
    wget -q --timeout=60 -O "${dest}" "${url}"
  else
    xyz_error "$(xyz_t "Need curl or wget to download files." "需要 curl 或 wget 才能下载文件。")"
    exit 1
  fi
  if [[ ! -s "${dest}" ]]; then
    xyz_error "$(xyz_t "Download failed or file is empty:" "下载失败或文件为空:") ${url}"
    exit 1
  fi
  if xyz_need_cmd sha256sum; then
    xyz_info "SHA256: $(sha256sum "${dest}" | awk '{print $1}')"
  elif xyz_need_cmd shasum; then
    xyz_info "SHA256: $(shasum -a 256 "${dest}" | awk '{print $1}')"
  fi
}

xyz_run_upstream() {
  # xyz_run_upstream URL [args...]
  # Downloads an official installer, shows the URL and a short preview, then
  # runs it only after confirmation (unless -y / YES=1).
  local url="$1"
  shift
  local dest
  xyz_init_tmpdir
  dest="$(xyz_tmpdir)/upstream.sh"
  xyz_fetch "${url}" "${dest}"
  chmod +x "${dest}"
  xyz_info "$(xyz_t "Preview (first 20 lines):" "预览(前 20 行):")"
  sed -n '1,20p' "${dest}" || true
  printf '%s\n' "${XYZ_C_DIM}...${XYZ_C_RESET}"
  # Default --dry-run: verify the download and preview, do not execute.
  # Scripts that forward --dry-run to upstream (e.g. docker) set
  # XYZ_FORWARD_DRY_RUN=1 so the official script still runs with that flag.
  if [[ "${XYZ_DRY_RUN:-0}" == "1" && "${XYZ_FORWARD_DRY_RUN:-0}" != "1" ]]; then
    xyz_info "$(xyz_t \
      "Dry-run: official script downloaded and previewed; not executing it." \
      "演练模式:已下载并预览官方脚本,不会执行。")"
    return 0
  fi
  if ! xyz_confirm "$(xyz_t \
    "Run this official upstream script?" \
    "运行这份官方上游脚本?")"; then
    xyz_error "$(xyz_t "Aborted by user." "已取消。")"
    exit 1
  fi
  xyz_step "$(xyz_t "Executing upstream script" "正在执行上游脚本")"
  # Never let the upstream script inherit our stdin: under `curl | bash` that
  # is the rest of this very script. Give it the terminal if there is one,
  # otherwise /dev/null. Interactive installers set XYZ_UPSTREAM_NEEDS_TTY=1.
  if xyz_open_tty; then
    local tty_fd="${XYZ_TTY_FD}"
    XYZ_TTY_FD=""
    bash "${dest}" "$@" <&"${tty_fd}"
    local rc=$?
    exec {tty_fd}>&- 2>/dev/null || true
    return "${rc}"
  fi
  if [[ "${XYZ_UPSTREAM_NEEDS_TTY:-0}" == "1" ]]; then
    xyz_error "$(xyz_t \
      "This official installer is interactive and needs a terminal. Log in with SSH and run: bash <(curl -fsSL ${XYZ_BASE}/${XYZ_NAME})" \
      "这个官方安装脚本需要交互,必须在终端里运行。请用 SSH 登录后执行:bash <(curl -fsSL ${XYZ_BASE}/${XYZ_NAME})")"
    exit 1
  fi
  bash "${dest}" "$@" </dev/null
}

# --- small shared helpers ----------------------------------------------------
xyz_strip_ansi() {
  sed -e 's/\x1b\[[0-9;?]*[A-Za-z]//g' -e 's/\r//g'
}

# Effective SSH port: sshd -T (what sshd really uses) > config files > env > 22.
xyz_effective_ssh_port() {
  local port=""
  if xyz_need_cmd sshd; then
    port="$(sshd -T 2>/dev/null | awk '$1=="port" {print $2; exit}' || true)"
  fi
  if [[ -z "${port}" && -d /etc/ssh/sshd_config.d ]]; then
    port="$(cat /etc/ssh/sshd_config.d/*.conf 2>/dev/null | awk 'tolower($1)=="port" {print $2; exit}' || true)"
  fi
  if [[ -z "${port}" && -f /etc/ssh/sshd_config ]]; then
    port="$(awk 'tolower($1)=="port" {print $2; exit}' /etc/ssh/sshd_config || true)"
  fi
  if [[ -z "${port}" && -n "${SSH_CONNECTION:-}" ]]; then
    port="$(printf '%s' "${SSH_CONNECTION}" | awk '{print $4}')"
  fi
  printf '%s' "${port:-22}"
}

xyz_has_systemd() {
  [[ -d /run/systemd/system ]] && xyz_need_cmd systemctl
}

# Back up a file once per run as FILE.xyz-bak-YYYYmmddHHMMSS and print the path.
xyz_backup_file() {
  local f="$1"
  local bak
  bak="${f}.xyz-bak-$(date +%Y%m%d%H%M%S)"
  if [[ -e "${f}" ]]; then
    cp -a "${f}" "${bak}"
    printf '%s' "${bak}"
  fi
}

# --- misc -------------------------------------------------------------------
xyz_usage_exit() {
  if [[ "${1:-}" != "" ]]; then
    printf '%s\n' "$1"
  fi
  exit 0
}

# xyz.sh terminal report helpers: frames, colored badges, bar charts.
# Shared by speedtest-cn, route, unlock and all. Pure bash, no telemetry.
#
# Colors: on only when stdout is a terminal (or XYZ_COLOR=always). Off with
# --no-color, NO_COLOR=1 or XYZ_COLOR=never.
# Glyphs: Unicode box drawing when the locale is UTF-8, otherwise ASCII
# (force with XYZ_ASCII=1 or XYZ_UTF8=1). Frames are 78 columns wide.

# shellcheck disable=SC2034
XYZ_UI_LOADED=1
XYZ_UI_WIDTH=78

xyz_ui_init() {
  local want="${XYZ_COLOR:-auto}"
  [[ "${want}" == "auto" && -n "${NO_COLOR:-}" ]] && want="never"
  [[ "${XYZ_NO_COLOR:-0}" == "1" ]] && want="never"
  XYZ_UI_COLOR=0
  case "${want}" in
    always) XYZ_UI_COLOR=1 ;;
    never) XYZ_UI_COLOR=0 ;;
    *) [[ -t 1 ]] && XYZ_UI_COLOR=1 ;;
  esac
  if [[ "${XYZ_UI_COLOR}" == "1" ]]; then
    U_X=$'\033[0m'; U_B=$'\033[1m'; U_D=$'\033[2m'
    U_RED=$'\033[31m'; U_G=$'\033[32m'; U_Y=$'\033[33m'
    U_BL=$'\033[34m'; U_M=$'\033[35m'; U_C=$'\033[36m'
  else
    U_X=""; U_B=""; U_D=""; U_RED=""; U_G=""; U_Y=""; U_BL=""; U_M=""; U_C=""
  fi
  XYZ_UI_UTF8=0
  if [[ "${XYZ_ASCII:-0}" != "1" ]]; then
    case "${LC_ALL:-${LC_CTYPE:-${LANG:-}}}" in
      *[Uu][Tt][Ff]-8*|*[Uu][Tt][Ff]8*) XYZ_UI_UTF8=1 ;;
      *) XYZ_UI_UTF8=0 ;;
    esac
    [[ "${XYZ_UTF8:-0}" == "1" ]] && XYZ_UI_UTF8=1
  fi
  if [[ "${XYZ_UI_UTF8}" == "1" ]]; then
    G_H="─"; G_V="│"; G_TL="╭"; G_TR="╮"; G_BL="╰"; G_BR="╯"; G_LT="├"; G_RT="┤"
    G_FULL="█"; G_EMPTY="░"; G_OK="✔"; G_BAD="✘"; G_UNK="?"; G_DOT="●"; G_STAR="★"
    G_DOWN="↓"; G_UP="↑"; G_WARN="⚠"; G_MID="·"; G_TIMES="×"
  else
    G_H="-"; G_V="|"; G_TL="+"; G_TR="+"; G_BL="+"; G_BR="+"; G_LT="+"; G_RT="+"
    G_FULL="#"; G_EMPTY="."; G_OK="+"; G_BAD="x"; G_UNK="?"; G_DOT="*"; G_STAR="*"
    G_DOWN="v"; G_UP="^"; G_WARN="!"; G_MID="-"; G_TIMES="x"
  fi
}

# Display width of a string: ANSI codes removed, CJK counted as 2 columns.
xyz_ui_w() {
  local s="$1" pre rest
  while [[ "${s}" == *$'\033['* ]]; do
    pre="${s%%$'\033['*}"
    rest="${s#*$'\033['}"
    rest="${rest#*m}"
    s="${pre}${rest}"
  done
  local LC_ALL=C
  local a="${s//[^ -~]/}"
  local one="${s//[^$'\xc2'-$'\xdf'$'\xe2']/}"
  local two="${s//[^$'\xe3'-$'\xef']/}"
  printf '%d' $(( ${#a} + ${#one} + 2 * ${#two} ))
}

xyz_ui_rep() {
  # xyz_ui_rep CHAR N
  local out="" n="$2"
  while (( n > 0 )); do out="${out}$1"; n=$((n - 1)); done
  printf '%s' "${out}"
}

xyz_ui_pad() {
  # xyz_ui_pad TEXT WIDTH  (left aligned, never truncates)
  local w
  w="$(xyz_ui_w "$1")"
  printf '%s%s' "$1" "$(xyz_ui_rep ' ' $(( $2 - w )))"
}

xyz_ui_lpad() {
  # xyz_ui_lpad TEXT WIDTH  (right aligned)
  local w
  w="$(xyz_ui_w "$1")"
  printf '%s%s' "$(xyz_ui_rep ' ' $(( $2 - w )))" "$1"
}

xyz_ui_top() {
  local title=" $1 " w
  w="$(xyz_ui_w "${title}")"
  printf '%s%s%s%s%s%s%s\n' "${U_C}" "${G_TL}${G_H}" "${U_X}${U_B}" "${title}" "${U_X}${U_C}" \
    "$(xyz_ui_rep "${G_H}" $(( XYZ_UI_WIDTH - 3 - w )))${G_TR}" "${U_X}"
}

xyz_ui_sep() {
  local label="${1:-}" w=0
  if [[ -n "${label}" ]]; then
    label=" ${label} "
    w="$(xyz_ui_w "${label}")"
    printf '%s%s%s%s%s%s%s\n' "${U_C}" "${G_LT}${G_H}" "${U_X}${U_B}" "${label}" "${U_X}${U_C}" \
      "$(xyz_ui_rep "${G_H}" $(( XYZ_UI_WIDTH - 3 - w )))${G_RT}" "${U_X}"
  else
    printf '%s%s%s\n' "${U_C}" "${G_LT}$(xyz_ui_rep "${G_H}" $(( XYZ_UI_WIDTH - 2 )))${G_RT}" "${U_X}"
  fi
}

xyz_ui_row() {
  local text="$1" inner=$(( XYZ_UI_WIDTH - 4 )) plain
  if (( $(xyz_ui_w "${text}") > inner )); then
    # too wide: drop colors and cut to fit, ending with "~"
    plain="$(printf '%s' "${text}" | sed 's/\x1b\[[0-9;]*m//g')"
    plain="${plain:0:inner}"
    while (( $(xyz_ui_w "${plain}") > inner - 1 )); do plain="${plain%?}"; done
    text="${plain}~"
  fi
  printf '%s%s%s %s %s%s%s\n' "${U_C}" "${G_V}" "${U_X}" "$(xyz_ui_pad "${text}" "${inner}")" "${U_C}" "${G_V}" "${U_X}"
}

xyz_ui_bottom() {
  printf '%s%s%s\n' "${U_C}" "${G_BL}$(xyz_ui_rep "${G_H}" $(( XYZ_UI_WIDTH - 2 )))${G_BR}" "${U_X}"
}

xyz_ui_bar() {
  # xyz_ui_bar VALUE MAX WIDTH [COLOR]  -> filled/empty bar (at least 1 cell if VALUE > 0)
  local n color="${4:-${U_G}}"
  n="$(awk -v v="${1:-0}" -v m="${2:-0}" -v w="$3" 'BEGIN {
    if (m <= 0 || v <= 0) { print 0; exit }
    n = int(v / m * w + 0.5); if (n < 1) n = 1; if (n > w) n = w; print n }')"
  printf '%s%s%s%s%s' "${color}" "$(xyz_ui_rep "${G_FULL}" "${n}")" "${U_D}" "$(xyz_ui_rep "${G_EMPTY}" $(( $3 - n )))" "${U_X}"
}

xyz_ui_usage() {
  # xyz_ui_usage PERCENT WIDTH -> colored usage bar + percent (red > 90, yellow > 75)
  local p="${1:-0}" color="${U_G}"
  p="${p%%.*}"; [[ "${p}" =~ ^[0-9]+$ ]] || p=0
  (( p > 75 )) && color="${U_Y}"
  (( p > 90 )) && color="${U_RED}"
  printf '%s %s%3d%%%s' "$(xyz_ui_bar "${p}" 100 "$2" "${color}")" "${color}" "${p}" "${U_X}"
}

xyz_ui_badge() {
  # xyz_ui_badge yes|no|partial|unknown|ok|skip [TEXT]
  local txt="${2:-}"
  case "$1" in
    yes|ok) printf '%s%s %s%s' "${U_G}" "${G_OK}" "${txt:-$(xyz_t "yes" "解锁")}" "${U_X}" ;;
    no|fail) printf '%s%s %s%s' "${U_RED}" "${G_BAD}" "${txt:-$(xyz_t "no" "不可用")}" "${U_X}" ;;
    partial) printf '%s%s %s%s' "${U_Y}" "${G_UNK}" "${txt:-$(xyz_t "partial" "部分")}" "${U_X}" ;;
    skip) printf '%s%s %s%s' "${U_D}" "-" "${txt:-$(xyz_t "skipped" "跳过")}" "${U_X}" ;;
    *) printf '%s%s %s%s' "${U_Y}" "${G_UNK}" "${txt:-$(xyz_t "unknown" "未知")}" "${U_X}" ;;
  esac
}

xyz_ui_line_class() {
  # premium | good | ordinary | other
  case "$1" in
    CN2-GIA|9929/CUII|9929|CMIN2) printf 'premium' ;;
    CN2-GT) printf 'good' ;;
    163|4837|CMI|CMNET) printf 'ordinary' ;;
    *) printf 'other' ;;
  esac
}

xyz_ui_linetags() {
  # xyz_ui_linetags "CN2-GIA 9929/CUII" -> colored tags
  local t out="" c
  for t in $1; do
    case "$(xyz_ui_line_class "${t}")" in
      premium) c="${U_G}${U_B}" ;;
      good) c="${U_C}" ;;
      ordinary) c="${U_Y}" ;;
      *) c="${U_D}" ;;
    esac
    out="${out}${out:+ }${c}${t}${U_X}"
  done
  printf '%s' "${out:-${U_D}?${U_X}}"
}

xyz_ui_fill() {
  # Replace empty TSV fields with "-" (bash read collapses consecutive tabs).
  awk -F'\t' -v OFS='\t' '{ for (i = 1; i <= NF; i++) if ($i == "") $i = "-"; print }' "$1"
}

xyz_ui_carrier() {
  case "$1" in
    nearest) xyz_t "Local" "本地" ;;
    ct) xyz_t "Telecom" "电信" ;;
    cu) xyz_t "Unicom" "联通" ;;
    cm) xyz_t "Mobile" "移动" ;;
    custom) xyz_t "Custom" "自定义" ;;
    hk) xyz_t "HK" "香港" ;;
    tokyo) xyz_t "Tokyo" "东京" ;;
    singapore) xyz_t "SGP" "新加坡" ;;
    us-west) xyz_t "US West" "美西" ;;
    *) printf '%s' "$1" ;;
  esac
}

xyz_ui_carrier_color() {
  case "$1" in
    ct|tokyo) printf '%s' "${U_BL}" ;;
    cu) printf '%s' "${U_RED}" ;;
    cm|singapore) printf '%s' "${U_G}" ;;
    hk) printf '%s' "${U_Y}" ;;
    *) printf '%s' "${U_M}" ;;
  esac
}

xyz_ui_fmt_num() {
  # 2 decimals below 1000, 1 decimal above
  awk -v v="${1:-0}" 'BEGIN { if (v >= 1000) printf "%.1f", v; else printf "%.2f", v }'
}

# xyz_ui_speed_rows FILE
# FILE: carrier id label down up ping loss status [engine] [note] (TSV, Mbps / ms)
xyz_ui_speed_rows() {
  local f="$1" max carrier id label down up lat loss status engine note head
  [[ -s "${f}" ]] || return 0
  max="$(awk -F'\t' '$8=="ok" { if ($4+0 > m) m = $4+0; if ($5+0 > m) m = $5+0 } END { print m+0 }' "${f}")"
  while IFS=$'\t' read -r carrier id label down up lat loss status engine note; do
    [[ "${engine}" == "-" ]] && engine=""
    head="$(xyz_ui_carrier_color "${carrier}")$(xyz_ui_pad "$(xyz_ui_carrier "${carrier}")" 8)${U_X}${U_B}${label}${U_X}${U_D} #${id}${engine:+ ${G_MID} ${engine}}${U_X}"
    xyz_ui_row "${head}"
    if [[ "${status}" == "ok" ]]; then
      local pc="${U_G}"
      lat="${lat:-?}"
      awk -v p="${lat}" 'BEGIN { exit !(p+0 > 150) }' 2>/dev/null && pc="${U_Y}"
      awk -v p="${lat}" 'BEGIN { exit !(p+0 > 250) }' 2>/dev/null && pc="${U_RED}"
      xyz_ui_row "  ${G_DOWN} $(xyz_ui_bar "${down}" "${max}" 14 "${U_G}") $(xyz_ui_lpad "$(xyz_ui_fmt_num "${down}")" 7) Mbps  ${G_UP} $(xyz_ui_bar "${up}" "${max}" 14 "${U_C}") $(xyz_ui_lpad "$(xyz_ui_fmt_num "${up}")" 7) Mbps  ${pc}$(xyz_ui_lpad "${lat}" 6)ms${U_X}"
    else
      [[ "${note}" == "-" ]] && note=""
      xyz_ui_row "  $(xyz_ui_badge skip "$(xyz_t "skipped" "跳过")")${U_D}${note:+ ${G_MID} ${note:0:62}}${U_X}"
    fi
  done < <(xyz_ui_fill "${f}")
}

xyz_ui_speed_table() {
  # xyz_ui_speed_table FILE TITLE
  xyz_ui_top "$2"
  xyz_ui_speed_rows "$1"
  xyz_ui_bottom
}

# Route grid. FILE: target label kind hops [key]   (key like bj-ct)
xyz_ui_route_key() {
  case "$1" in
    219.141.140.10) printf 'bj-ct' ;; 202.106.195.68) printf 'bj-cu' ;; 221.179.155.161) printf 'bj-cm' ;;
    202.96.209.133) printf 'sh-ct' ;; 210.22.97.1) printf 'sh-cu' ;; 211.136.112.200) printf 'sh-cm' ;;
    58.60.188.222) printf 'gz-ct' ;; 210.21.196.6) printf 'gz-cu' ;; 120.196.165.24) printf 'gz-cm' ;;
    *) printf '' ;;
  esac
}

xyz_ui_route_rows() {
  local f="$1" target label kind hops key city c cell line extra=0
  local -A K=()
  [[ -s "${f}" ]] || return 0
  while IFS=$'\t' read -r target label kind hops key; do
    [[ -z "${key}" || "${key}" == "-" ]] && key="$(xyz_ui_route_key "${target}")"
    if [[ -n "${key}" ]]; then
      K["${key}"]="${kind}"
    else
      extra=1
    fi
  done < <(xyz_ui_fill "${f}")
  if (( ${#K[@]} > 0 )); then
    xyz_ui_row "${U_D}$(xyz_ui_pad "" 10)$(xyz_ui_pad "$(xyz_ui_carrier ct)" 21)$(xyz_ui_pad "$(xyz_ui_carrier cu)" 21)$(xyz_ui_carrier cm)${U_X}"
    for city in bj sh gz; do
      case "${city}" in
        bj) line="$(xyz_ui_pad "$(xyz_t "Beijing" "北京")" 10)" ;;
        sh) line="$(xyz_ui_pad "$(xyz_t "Shanghai" "上海")" 10)" ;;
        *) line="$(xyz_ui_pad "$(xyz_t "Guangzhou" "广州")" 10)" ;;
      esac
      for c in ct cu cm; do
        cell="${K[${city}-${c}]-}"
        [[ "${cell}" == *" ("* || "${cell}" == unknown* || "${cell}" == 未识别* ]] && cell="?"
        if [[ -z "${cell}" ]]; then cell="${U_D}-${U_X}"; else cell="$(xyz_ui_linetags "${cell}")"; fi
        line="${line}$(xyz_ui_pad "${cell}" 21)"
      done
      xyz_ui_row "${line}"
    done
  fi
  if [[ "${extra}" == "1" ]]; then
    while IFS=$'\t' read -r target label kind hops key; do
      [[ -z "${key}" || "${key}" == "-" ]] && key="$(xyz_ui_route_key "${target}")"
      [[ -n "${key}" ]] && continue
      xyz_ui_row "$(xyz_ui_pad "${label:-${target}}" 22) $(xyz_ui_linetags "${kind}")"
    done < <(xyz_ui_fill "${f}")
  fi
  xyz_ui_row "${U_D}$(xyz_t "Legend:" "图例:")${U_X} ${U_G}${U_B}$(xyz_t "premium" "优质")${U_X} CN2-GIA/9929/CMIN2  ${U_C}CN2-GT${U_X}  ${U_Y}$(xyz_t "ordinary" "普通")${U_X} 163/4837/CMI"
}

xyz_ui_route_score() {
  # prints 0-100 (average best class per target) or nothing
  local f="$1" kind t best n=0 sum=0 cls
  [[ -s "${f}" ]] || return 0
  while IFS=$'\t' read -r _ _ kind _ _; do
    best=0
    for t in ${kind}; do
      cls="$(xyz_ui_line_class "${t}")"
      case "${cls}" in premium) best=100 ;; good) (( best < 75 )) && best=75 ;; ordinary) (( best < 40 )) && best=40 ;; *) : ;; esac
    done
    n=$((n + 1)); sum=$((sum + best))
  done < "${f}"
  (( n > 0 )) && printf '%d' $(( sum / n ))
}

# Unlock grid. FILE: service status region note
xyz_ui_unlock_rows() {
  local f="$1" svc st reg note cells=() i cell
  [[ -s "${f}" ]] || return 0
  while IFS=$'\t' read -r svc st reg note; do
    cell="$(xyz_ui_badge "${st}" "$(xyz_ui_pad "${svc}" 16)")"
    [[ "${reg}" != "-" ]] && cell="${cell} ${U_D}${reg:0:12}${U_X}"
    cells+=("${cell}")
  done < <(xyz_ui_fill "${f}")
  for (( i = 0; i < ${#cells[@]}; i += 2 )); do
    xyz_ui_row "$(xyz_ui_pad "${cells[$i]}" 37)${cells[$((i + 1))]-}"
  done
}

xyz_ui_unlock_count() {
  # prints "yes total"
  awk -F'\t' '{ n++; if ($2 == "yes") y++ } END { printf "%d %d\n", y, n }' "$1"
}

xyz_ui_init

XYZ_NAME="media"

# xyz.sh/unlock — streaming / AI service availability and IP quality check.
#
# A small self-written checker using plain curl requests (no installs):
#   Netflix, Disney+, YouTube Premium, ChatGPT, Claude, Gemini, TikTok
# plus IP information (ASN, country, datacenter / residential / mobile,
# VPN / proxy flags) from public free APIs:
#   https://api.ipapi.is  (primary)   http://ip-api.com (fallback)
#
# These checks are heuristics based on what each site returns to this IP
# right now. Sites change often; treat results as a hint, not a guarantee.
# For a much larger matrix, the well-known upstream projects can be run
# instead (downloaded, previewed, then executed only after confirmation):
#   --full       lmc999/RegionRestrictionCheck  https://github.com/lmc999/RegionRestrictionCheck
#   --ipquality  xykt/IPQuality                 https://github.com/xykt/IPQuality
#
# Usage:
#   curl -fsSL xyz.sh/unlock | bash
#   curl -fsSL xyz.sh/unlock | bash -s -- -6
#   curl -fsSL xyz.sh/unlock | bash -s -- --json
#   curl -fsSL xyz.sh/unlock | bash -s -- --no-ip
#   curl -fsSL xyz.sh/unlock | bash -s -- --full
#   curl -fsSL xyz.sh/unlock | bash -s -- --ipquality
#
# Root is not required. Nothing is installed or written (except a temp dir).
# The IP lookup sends your server's IP to the API above (that is the lookup);
# skip it with --no-ip. No telemetry is added by xyz.sh.

set -euo pipefail

if [[ -z "${XYZ_COMMON_LOADED:-}" ]]; then
  _xyz_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  if [[ -f "${_xyz_dir}/lib/common.sh" ]]; then
    # shellcheck source=lib/common.sh
    source "${_xyz_dir}/lib/common.sh"
  else
    echo "xyz.sh: missing lib/common.sh" >&2
    exit 1
  fi
fi

if [[ -z "${XYZ_UI_LOADED:-}" ]]; then
  _xyz_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  # shellcheck source=lib/report.sh
  source "${_xyz_dir}/lib/report.sh"
fi
XYZ_NAME="${XYZ_NAME:-unlock}"
case "${XYZ_NAME}" in unlock|ipcheck|media) : ;; *) XYZ_NAME="unlock" ;; esac
XYZ_TITLE="$(xyz_t "Streaming / AI unlock and IP quality check" "流媒体 / AI 解锁与 IP 质量检测")"
RRC_URL="https://raw.githubusercontent.com/lmc999/RegionRestrictionCheck/main/check.sh"
IPQ_URL="https://raw.githubusercontent.com/xykt/IPQuality/main/ip.sh"

# With --json, keep stdout clean: banner and logs go to stderr.
exec 3>&1
for _arg in "$@"; do
  if [[ "${_arg}" == "--json" ]]; then
    exec 1>&2
  fi
done

xyz_init "$@"

IPV="4"
WANT_JSON=0
NO_IP=0
DO_FULL=0
DO_IPQ=0
for arg in "${XYZ_ARGS[@]+"${XYZ_ARGS[@]}"}"; do
  case "${arg}" in
    -4) IPV="4" ;;
    -6) IPV="6" ;;
    --json) WANT_JSON=1 ;;
    --no-ip) NO_IP=1 ;;
    --full) DO_FULL=1 ;;
    --ipquality) DO_IPQ=1 ;;
    --help|-h) XYZ_HELP=1 ;;
    *)
      xyz_error "$(xyz_t "Unknown argument:" "未知参数:") ${arg}"
      exit 1
      ;;
  esac
done

if [[ "${XYZ_HELP:-}" == "1" ]]; then
  cat <<HINT
$(xyz_t "Usage:" "用法:")
  curl -fsSL xyz.sh/unlock | bash
  curl -fsSL xyz.sh/unlock | bash -s -- -6
  curl -fsSL xyz.sh/unlock | bash -s -- --json
  curl -fsSL xyz.sh/unlock | bash -s -- --no-ip
  curl -fsSL xyz.sh/unlock | bash -s -- --full        # lmc999/RegionRestrictionCheck
  curl -fsSL xyz.sh/unlock | bash -s -- --ipquality   # xykt/IPQuality

$(xyz_t "Checks: Netflix, Disney+, YouTube Premium, ChatGPT, Claude, Gemini, TikTok, and IP type." "检测:Netflix、Disney+、YouTube Premium、ChatGPT、Claude、Gemini、TikTok 以及 IP 类型。")
$(xyz_t "Results are heuristics; websites change their responses often." "结果为启发式判断,网站返回内容经常变化,仅供参考。")
HINT
  exit 0
fi

if [[ "${DO_FULL}" == "1" ]]; then
  XYZ_UPSTREAM="https://github.com/lmc999/RegionRestrictionCheck"
  xyz_info "$(xyz_t "Running the upstream RegionRestrictionCheck by lmc999 (AGPL-3.0)." "运行 lmc999 的上游 RegionRestrictionCheck(AGPL-3.0)。")"
  xyz_info "Project: ${XYZ_UPSTREAM}"
  XYZ_UPSTREAM_NEEDS_TTY=1
  xyz_run_upstream "${RRC_URL}"
  exit 0
fi
if [[ "${DO_IPQ}" == "1" ]]; then
  XYZ_UPSTREAM="https://github.com/xykt/IPQuality"
  xyz_info "$(xyz_t "Running the upstream IPQuality by xykt (AGPL-3.0)." "运行 xykt 的上游 IPQuality(AGPL-3.0)。")"
  xyz_info "Project: ${XYZ_UPSTREAM}"
  xyz_run_upstream "${IPQ_URL}" "-${IPV}"
  exit 0
fi

if ! xyz_need_cmd curl; then
  xyz_error "$(xyz_t "curl is required." "需要 curl。")"
  exit 1
fi

UA="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36"
xyz_init_tmpdir
TMP="$(xyz_tmpdir)"

# fetch NAME URL [extra curl args...] -> body in $TMP/NAME.body, prints "code effective_url"
fetch() {
  local name="$1" url="$2"
  shift 2
  curl "-${IPV}" -sS -L --max-time 12 --connect-timeout 6 -A "${UA}" \
    -H 'Accept-Language: en-US,en;q=0.9' "$@" \
    -o "${TMP}/${name}.body" -w '%{http_code} %{url_effective}' "${url}" 2>/dev/null || printf '000 %s' "${url}"
}

body_has() { grep -q -i -- "$2" "${TMP}/$1.body" 2>/dev/null; }

RESULTS=()
add() {
  # add SERVICE STATUS REGION NOTE   (STATUS: yes|no|partial|unknown)
  RESULTS+=("$1|$2|$3|$4")
}

check_netflix() {
  local a b ra rb region=""
  a="$(fetch nf1 https://www.netflix.com/title/81280792)"
  b="$(fetch nf2 https://www.netflix.com/title/70143836)"
  ra="${a%% *}"; rb="${b%% *}"
  region="$(printf '%s\n%s\n' "${a#* }" "${b#* }" | grep -o -E 'netflix\.com/[a-z]{2}(-[a-z]{2})?/' | head -1 | sed -E 's#netflix\.com/([a-z]{2}).*#\1#' | tr '[:lower:]' '[:upper:]' || true)"
  if [[ -z "${region}" ]]; then
    region="$(grep -o -E '"requestCountry":\{"id":"[A-Z]{2}"' "${TMP}/nf1.body" 2>/dev/null | grep -o -E '[A-Z]{2}"$' | tr -d '"' | head -1 || true)"
  fi
  if [[ "${ra}" == "000" && "${rb}" == "000" ]]; then
    add "Netflix" unknown "" "$(xyz_t "network error" "网络错误")"
  elif [[ "${ra}" == "200" || "${rb}" == "200" ]]; then
    if [[ "${ra}" == "200" && "${rb}" == "200" ]]; then
      add "Netflix" yes "${region:-US}" "$(xyz_t "full catalogue (non-originals)" "完整解锁(含非自制剧)")"
    else
      add "Netflix" partial "${region:-US}" "$(xyz_t "some licensed titles" "部分非自制剧")"
    fi
  elif [[ "${ra}" == "404" && "${rb}" == "404" ]]; then
    add "Netflix" partial "${region}" "$(xyz_t "originals only" "仅自制剧")"
  elif [[ "${ra}" == "403" || "${rb}" == "403" ]]; then
    add "Netflix" no "" "$(xyz_t "blocked (HTTP 403)" "被屏蔽(HTTP 403)")"
  else
    add "Netflix" unknown "${region}" "HTTP ${ra}/${rb}"
  fi
}

check_disney() {
  local r code url
  r="$(fetch dp https://www.disneyplus.com/)"
  code="${r%% *}"; url="${r#* }"
  if [[ "${code}" == "000" ]]; then
    add "Disney+" unknown "" "$(xyz_t "network error" "网络错误")"
  elif [[ "${url}" == *unavailable* ]] || body_has dp 'not available in your region'; then
    add "Disney+" no "" "$(xyz_t "redirected to 'unavailable'" "跳转到不可用页面")"
  elif [[ "${code}" == "403" ]]; then
    add "Disney+" no "" "HTTP 403"
  elif [[ "${code}" == "200" ]]; then
    local region
    region="$(grep -o -E '"countryCode":"[A-Z]{2}"' "${TMP}/dp.body" | head -1 | sed -E 's/.*:"([A-Z]{2})"/\1/' || true)"
    [[ -z "${region}" ]] && region="$(printf '%s' "${url}" | grep -o -E 'disneyplus\.com/[a-z]{2}-([a-z]{2})' | sed -E 's#.*-##' | tr '[:lower:]' '[:upper:]' || true)"
    add "Disney+" yes "${region}" "$(xyz_t "site served (sign-up not tested)" "网站可访问(未测试注册)")"
  else
    add "Disney+" unknown "" "HTTP ${code}"
  fi
}

check_youtube() {
  local r code region
  r="$(fetch yt https://www.youtube.com/premium)"
  code="${r%% *}"
  region="$(grep -o -E '"(INNERTUBE_CONTEXT_)?GL":"[A-Z]{2}"' "${TMP}/yt.body" 2>/dev/null | head -1 | sed -E 's/.*:"([A-Z]{2})"/\1/' || true)"
  [[ -z "${region}" ]] && region="$(grep -o -E '"countryCode":"[A-Z]{2}"' "${TMP}/yt.body" 2>/dev/null | head -1 | sed -E 's/.*:"([A-Z]{2})"/\1/' || true)"
  if [[ "${code}" == "000" ]]; then
    add "YouTube Premium" unknown "" "$(xyz_t "network error" "网络错误")"
  elif body_has yt 'Premium is not available in your country'; then
    add "YouTube Premium" no "${region}" "$(xyz_t "not available in this country" "此地区不可用")"
  elif [[ "${region}" == "CN" ]]; then
    add "YouTube Premium" no "CN" ""
  elif [[ "${code}" == "200" ]] && (body_has yt 'ad-free' || body_has yt 'YouTube Premium'); then
    add "YouTube Premium" yes "${region}" ""
  else
    add "YouTube Premium" unknown "${region}" "HTTP ${code}"
  fi
}

check_chatgpt() {
  local r1 r2 c1 c2 loc
  r1="$(fetch oa1 https://api.openai.com/compliance/cookie_requirements -H 'authorization: Bearer null')"
  r2="$(fetch oa2 https://ios.chat.openai.com/)"
  c1="${r1%% *}"; c2="${r2%% *}"
  fetch oatrace https://chatgpt.com/cdn-cgi/trace >/dev/null
  loc="$(awk -F= '$1=="loc"{print $2}' "${TMP}/oatrace.body" 2>/dev/null | head -1 || true)"
  if [[ "${c1}" == "000" && "${c2}" == "000" ]]; then
    add "ChatGPT" unknown "" "$(xyz_t "network error" "网络错误")"
  elif body_has oa1 'unsupported_country'; then
    add "ChatGPT" no "${loc}" "$(xyz_t "unsupported country" "不支持的地区")"
  elif body_has oa2 'VPN' ; then
    add "ChatGPT" partial "${loc}" "$(xyz_t "web only; app flags this IP as VPN" "仅网页;App 判定为 VPN")"
  else
    add "ChatGPT" yes "${loc}" ""
  fi
}

check_claude() {
  local r code url
  r="$(fetch cl https://claude.ai/)"
  code="${r%% *}"; url="${r#* }"
  if [[ "${code}" == "000" ]]; then
    add "Claude" unknown "" "$(xyz_t "network error" "网络错误")"
  elif [[ "${url}" == *unavailable* ]]; then
    add "Claude" no "" "$(xyz_t "app-unavailable-in-region" "地区不可用")"
  elif [[ "${code}" == "403" ]]; then
    add "Claude" unknown "" "$(xyz_t "HTTP 403 (bot challenge or block)" "HTTP 403(人机验证或屏蔽)")"
  else
    add "Claude" yes "" "HTTP ${code}"
  fi
}

check_gemini() {
  local r code region
  r="$(fetch gm https://gemini.google.com/)"
  code="${r%% *}"
  region="$(grep -o -E ',2,1,200,"[A-Z]{3}"' "${TMP}/gm.body" 2>/dev/null | head -1 | sed -E 's/.*"([A-Z]{3})"/\1/' || true)"
  if [[ "${code}" == "000" ]]; then
    add "Gemini" unknown "" "$(xyz_t "network error" "网络错误")"
  elif grep -q -E '45631641(,null,true|\\?":true)' "${TMP}/gm.body" 2>/dev/null; then
    add "Gemini" yes "${region}" ""
  elif [[ "${code}" == "200" ]]; then
    add "Gemini" unknown "${region}" "$(xyz_t "availability flag not found" "未找到可用标记")"
  else
    add "Gemini" unknown "${region}" "HTTP ${code}"
  fi
}

check_tiktok() {
  local r code region
  r="$(fetch tt https://www.tiktok.com/explore)"
  code="${r%% *}"
  region="$(grep -o -E '"region":"[A-Z]{2}"' "${TMP}/tt.body" 2>/dev/null | head -1 | sed -E 's/.*:"([A-Z]{2})"/\1/' || true)"
  if [[ "${code}" == "000" ]]; then
    add "TikTok" unknown "" "$(xyz_t "network error" "网络错误")"
  elif [[ -n "${region}" ]]; then
    add "TikTok" yes "${region}" ""
  elif [[ "${code}" == "200" ]]; then
    add "TikTok" partial "" "$(xyz_t "page served, region not detected" "页面可访问,未识别地区")"
  else
    add "TikTok" no "" "HTTP ${code}"
  fi
}

# ---- IP information ----------------------------------------------------------
IP_ADDR=""; IP_ASN=""; IP_ORG=""; IP_CC=""; IP_TYPE=""; IP_FLAGS=""
json_str() { grep -o -E "\"$1\": *\"[^\"]*\"" "$2" 2>/dev/null | head -1 | sed -E 's/.*: *"(.*)"/\1/' || true; }
json_bool() { grep -o -E "\"$1\": *(true|false)" "$2" 2>/dev/null | head -1 | grep -o -E 'true|false' || true; }
json_num() { grep -o -E "\"$1\": *[0-9]+" "$2" 2>/dev/null | head -1 | grep -o -E '[0-9]+$' || true; }

DC_WORDS='cloud|hosting|host|datacenter|data center|server|vps|colo|cloudflare|amazon|aws|google|microsoft|azure|oracle|digitalocean|linode|akamai|vultr|choopa|ovh|hetzner|contabo|alibaba|aliyun|tencent|huawei cloud|bandwagon|it7|leaseweb|m247|psychz|cogent|zenlayer|dmit|racknerd|buyvm|frantech|greencloud|kamatera|scaleway|upcloud|ionos|netcup|godaddy|hostinger|gcore|g-core'

check_ip() {
  local f="${TMP}/ipapi.body" f2="${TMP}/ipapi2.body" f3="${TMP}/ipinfo.body"
  local dc="" mobile="" vpn="" proxy="" tor="" abuser="" ctype=""
  fetch ipapi https://api.ipapi.is/ >/dev/null
  if [[ -s "${f}" ]] && grep -q '"ip"' "${f}"; then
    IP_SRC="api.ipapi.is"
    IP_ADDR="$(json_str ip "${f}")"
    IP_ASN="$(grep -o -E '"asn": *"AS[0-9]+' "${f}" | head -1 | grep -o -E 'AS[0-9]+' || true)"
    [[ -z "${IP_ASN}" ]] && IP_ASN="$(json_num asn "${f}")" && [[ -n "${IP_ASN}" ]] && IP_ASN="AS${IP_ASN}"
    IP_ORG="$(json_str company "${f}")"
    [[ -z "${IP_ORG}" ]] && IP_ORG="$(grep -o -E '"company": *\{"name": *"[^"]*"' "${f}" | head -1 | sed -E 's/.*"([^"]*)"$/\1/' || true)"
    IP_CC="$(json_str country_code "${f}")"
    [[ -z "${IP_CC}" ]] && IP_CC="$(json_str country "${f}")"
    dc="$(json_bool is_datacenter "${f}")"; mobile="$(json_bool is_mobile "${f}")"
    vpn="$(json_bool is_vpn "${f}")"; proxy="$(json_bool is_proxy "${f}")"
    tor="$(json_bool is_tor "${f}")"; abuser="$(json_bool is_abuser "${f}")"
    ctype="$(grep -o -E '"company": *\{[^}]*"type": *"[a-z]+"' "${f}" | head -1 | sed -E 's/.*"([a-z]+)"$/\1/' || true)"
  fi
  # ip-api.com has hosting / proxy / mobile flags (free endpoint is HTTP only).
  fetch ipapi2 'http://ip-api.com/json/?fields=status,countryCode,isp,org,as,mobile,proxy,hosting,query' >/dev/null
  if grep -q '"status":"success"' "${f2}" 2>/dev/null; then
    IP_SRC="${IP_SRC:+${IP_SRC}, }ip-api.com"
    [[ -z "${IP_ADDR}" ]] && IP_ADDR="$(json_str query "${f2}")"
    [[ -z "${IP_ASN}" ]] && IP_ASN="$(json_str as "${f2}" | awk '{print $1}')"
    [[ -z "${IP_ORG}" ]] && IP_ORG="$(json_str org "${f2}")"
    [[ -z "${IP_CC}" ]] && IP_CC="$(json_str countryCode "${f2}")"
    [[ -z "${dc}" ]] && dc="$(json_bool hosting "${f2}")"
    [[ -z "${mobile}" ]] && mobile="$(json_bool mobile "${f2}")"
    [[ -z "${proxy}" ]] && proxy="$(json_bool proxy "${f2}")"
  fi
  if [[ -z "${IP_ADDR}" ]]; then
    fetch ipinfo https://ipinfo.io/json >/dev/null
    if grep -q '"ip"' "${f3}" 2>/dev/null; then
      IP_SRC="ipinfo.io"
      IP_ADDR="$(json_str ip "${f3}")"
      IP_ASN="$(json_str org "${f3}" | awk '{print $1}')"
      IP_ORG="$(json_str org "${f3}" | cut -d' ' -f2-)"
      IP_CC="$(json_str country "${f3}")"
    fi
  fi
  [[ -z "${IP_ADDR}" ]] && return 1
  if [[ "${mobile}" == "true" ]]; then
    IP_TYPE="$(xyz_t "mobile" "移动网络")"
  elif [[ "${dc}" == "true" || "${ctype}" == "hosting" ]]; then
    IP_TYPE="$(xyz_t "datacenter" "机房 IP")"
  elif [[ "${dc}" == "false" ]]; then
    IP_TYPE="$(xyz_t "residential / ISP" "住宅 / 运营商")"
  elif printf '%s' "${IP_ORG}" | grep -q -i -E "${DC_WORDS}"; then
    IP_TYPE="$(xyz_t "datacenter (guessed from ASN name)" "机房 IP(按 ASN 名称推断)")"
  else
    IP_TYPE="$(xyz_t "unknown (likely ISP)" "未知(可能是运营商 IP)")"
  fi
  [[ -n "${ctype}" && "${ctype}" != "hosting" ]] && IP_TYPE="${IP_TYPE} (${ctype})"
  [[ "${vpn}" == "true" ]] && IP_FLAGS="${IP_FLAGS} VPN"
  [[ "${proxy}" == "true" ]] && IP_FLAGS="${IP_FLAGS} proxy"
  [[ "${tor}" == "true" ]] && IP_FLAGS="${IP_FLAGS} Tor"
  [[ "${abuser}" == "true" ]] && IP_FLAGS="${IP_FLAGS} abuser"
  IP_FLAGS="${IP_FLAGS# }"
  return 0
}

xyz_step "$(xyz_t "Checking (IPv${IPV})" "正在检测(IPv${IPV})")"
IP_SRC=""
if [[ "${NO_IP}" != "1" ]]; then
  xyz_info "$(xyz_t "IP lookup via api.ipapi.is (fallback ip-api.com). Skip with --no-ip." "IP 信息查询:api.ipapi.is(备用 ip-api.com),可用 --no-ip 跳过。")"
  check_ip || xyz_warn "$(xyz_t "IP lookup failed." "IP 信息查询失败。")"
fi
check_netflix
check_disney
check_youtube
check_chatgpt
check_claude
check_gemini
check_tiktok

if [[ "${WANT_JSON}" == "1" ]]; then
  esc() { printf '%s' "$1" | sed 's/\\/\\\\/g;s/"/\\"/g'; }
  {
  printf '{\n  "ip": {"address": "%s", "asn": "%s", "org": "%s", "country": "%s", "type": "%s", "flags": "%s", "source": "%s"},\n  "services": [\n' \
    "$(esc "${IP_ADDR}")" "$(esc "${IP_ASN}")" "$(esc "${IP_ORG}")" "$(esc "${IP_CC}")" "$(esc "${IP_TYPE}")" "$(esc "${IP_FLAGS}")" "$(esc "${IP_SRC}")"
  n=0
  for r in "${RESULTS[@]}"; do
    IFS='|' read -r svc st reg note <<< "${r}"
    n=$((n + 1))
    sep=","; [[ ${n} -eq ${#RESULTS[@]} ]] && sep=""
    printf '    {"service": "%s", "status": "%s", "region": "%s", "note": "%s"}%s\n' "$(esc "${svc}")" "${st}" "$(esc "${reg}")" "$(esc "${note}")" "${sep}"
  done
  printf '  ]\n}\n'
  } >&3
  exit 0
fi

echo
xyz_init_tmpdir
UNLOCK_VIEW="$(xyz_tmpdir)/unlock-view.tsv"
: > "${UNLOCK_VIEW}"
for r in "${RESULTS[@]}"; do
  IFS='|' read -r svc st reg note <<< "${r}"
  printf '%s\t%s\t%s\t%s\n' "${svc}" "${st}" "${reg}" "${note}" >> "${UNLOCK_VIEW}"
  if [[ -n "${XYZ_RESULT_FILE:-}" ]]; then
    printf 'unlock\t%s\t%s\t%s\t%s\n' "${svc}" "${st}" "${reg}" "${note}" >> "${XYZ_RESULT_FILE}" 2>/dev/null || true
  fi
done
read -r U_YES U_TOTAL < <(xyz_ui_unlock_count "${UNLOCK_VIEW}")
xyz_ui_top "$(xyz_t "Streaming / AI unlock" "流媒体 / AI 解锁") ${U_YES}/${U_TOTAL}"
if [[ "${NO_IP}" != "1" ]]; then
  ip_type_c="${U_Y}"
  [[ "${IP_TYPE}" == residential* || "${IP_TYPE}" == isp* ]] && ip_type_c="${U_G}"
  xyz_ui_row "$(xyz_ui_pad "IP" 10)${IP_ADDR:-n/a}  ${U_D}${IP_ASN:-} ${IP_ORG:0:30}${U_X}"
  xyz_ui_row "$(xyz_ui_pad "$(xyz_t "Location" "位置")" 10)${IP_CC:-n/a} ${G_MID} ${ip_type_c}${IP_TYPE:-n/a}${U_X}${IP_FLAGS:+ ${G_MID} ${U_Y}${IP_FLAGS}${U_X}}  ${U_D}(${IP_SRC:-n/a})${U_X}"
  xyz_ui_sep
fi
xyz_ui_unlock_rows "${UNLOCK_VIEW}"
notes="$(awk -F'\t' '$2 != "yes" && $4 != "" { printf "%s: %s\n", $1, $4 }' "${UNLOCK_VIEW}")"
if [[ -n "${notes}" ]]; then
  xyz_ui_sep
  while IFS= read -r line; do xyz_ui_row "${U_D}${line:0:72}${U_X}"; done <<< "${notes}"
fi
xyz_ui_bottom
if [[ -n "${XYZ_RESULT_FILE:-}" && "${NO_IP}" != "1" ]]; then
  printf 'ip\t%s\t%s\t%s\t%s\t%s\n' "${IP_CC}" "${IP_ASN}" "${IP_TYPE}" "${IP_FLAGS}" "${IP_ORG}" >> "${XYZ_RESULT_FILE}" 2>/dev/null || true
fi
echo
xyz_info "$(xyz_t \
  "Heuristic results from this IP right now. For a larger matrix: --full (RegionRestrictionCheck) or --ipquality (IPQuality)." \
  "以上为当前 IP 的启发式检测结果。更全面的检测:--full(RegionRestrictionCheck)或 --ipquality(IPQuality)。")"

Recommended VPS

These slots are placeholders. No vendor or affiliate link has been added, and nothing here is a purchase button. Real plans and compliant referral links will be filled in later.

Suggested for:United States

Placeholder · US West

Vendor TBD

Region, specs, and price will be filled in after onboarding. This is not a checkout link.

Placeholder · Value

Vendor TBD

Region, specs, and price will be filled in after onboarding. This is not a checkout link.

Placeholder · High RAM / storage

Vendor TBD

Region, specs, and price will be filled in after onboarding. This is not a checkout link.