#!/usr/bin/env bash set -euo pipefail # cURL configurations applied to all requests CURL_ARGS=(--silent --max-time 6) # Color definitions COLOR_DANGER=$(tput setaf 1) COLOR_SUCCESS=$(tput setaf 2) COLOR_RESET=$(tput sgr0) show_usage () { echo "Usage: $0 [COMMAND] [OPTIONS]" echo "" echo "Commands:" echo " make:playlist Make a playlist file" echo " make:epg Make an EPG file" echo " decrypt:authinfo Find a possible IPTV key" echo "" echo "Run '$0 COMMAND -h' for help on a specific command." } show_make_playlist_usage () { echo "Usage: $0 make:playlist -u -p -d --mac " echo "" echo "Options:" echo " -u | --user User ID" echo " -p | --password Password" echo " -d | --device Device ID" echo " --ip Device IP address (default: 127.0.0.1)" echo " --mac Device MAC address" echo " --udpxy Udpxy endpoint (default: http://127.0.0.1:4022)" echo " -o | --output Playlist file name (default: playlist.M3U8)" echo " -I | --interface Network interface for cURL requests (optional)" } show_make_epg_usage () { echo "Usage: $0 make:epg" echo "" echo "Options:" echo " --endpoint EPG index endpoint (default: http://120.87.12.38:8083/epg/api/page/biz_59417088.json)" echo " -o | --output EPG file name (default: epg.xml)" echo " -I | --interface Network interface for cURL requests (optional)" } show_decrypt_authinfo_usage () { echo "Usage: $0 decrypt:authinfo " } step () { printf '%s==> %s%s\n' "$COLOR_SUCCESS" "$1" "$COLOR_RESET"; } fatal () { printf '%s!!! %s%s\n' "$COLOR_DANGER" "$1" "$COLOR_RESET" >&2; exit 1; } ensure_curl_is_installed () { if ! command -v curl &>/dev/null; then fatal "The cURL package that handles HTTP requests is not installed." fi } ensure_jq_is_installed () { if ! command -v jq &>/dev/null; then fatal "The jq package that handles HTTP JSON response is not installed." fi } ensure_openssl_is_installed () { if ! command -v openssl &>/dev/null; then fatal "The OpenSSL package that handles token computation is not installed." fi } categorize_by_channel_name () { result="720P" case "$1" in *4K*) result="4K" ;; *"超清"*|*"高清"*) result="1080P" ;; esac echo "$result" } retrieve_endpoint () { local user_id="$1" local url="http://eds1.unicomgd.com:8082/EDS/jsp/AuthenticationURL?Action=Login&UserID=$user_id&return_type=1" local response local epgurl response=$(curl "${CURL_ARGS[@]}" "$url") || return 1 epgurl=$(printf '%s' "$response" | jq -r '.epgurl') printf '%s\n' "$(extract_hostname "$epgurl")" } extract_hostname () { local url="$1" local res res="${url#*//}" res="${res%%/*}" printf '%s\n' "$res" } retrieve_encry_token () { local endpoint="$1" local user_id="$2" local url="http://$endpoint/EPG/oauth/v2/authorize?response_type=EncryToken&client_id=jiulian&userid=$user_id" local response local token response=$(curl "${CURL_ARGS[@]}" "$url") || return 1 token=$(printf '%s' "$response" | jq -r '.EncryToken') || return 1 [[ -n "$token" && "$token" != "null" ]] || return 1 printf '%s\n' "$token" } build_authinfo () { local encry_token="$1" local user_id="$2" local device_id="$3" local ip_address="$4" local mac_address="$5" local random_num=$(( (RANDOM << 15 | RANDOM) % 99999999 + 1 )) local parts=("$random_num" "$encry_token" "$user_id" "$device_id" "$ip_address" "$mac_address" "Reserved" "OTT") local joined joined=$(IFS='$'; printf '%s' "${parts[*]}") printf '%s\n' "$joined" } encrypt_authinfo () { local data="$1" local key="$2" local res res=$(printf '%s' "$data" | openssl enc -e -des-ede3 -K "$key" -nosalt 2>/dev/null | xxd -p -u -c 0) printf '%s\n' "$res" } retrieve_access_token () { local endpoint="$1" local user_id="$2" local authinfo="$3" local url="http://$endpoint/EPG/oauth/v2/token?grant_type=EncryToken&client_id=jiulian&UserID=$user_id&DeviceType=UNT400G&DeviceVersion=5.5.021&authinfo=$authinfo&issmarthomestb=1&tvdesktopid=" local response local token response=$(curl "${CURL_ARGS[@]}" "$url") || return 1 token=$(printf '%s' "$response" | jq -r '.access_token') || return 1 [[ -n "$token" && "$token" != "null" ]] || return 1 printf '%s\n' "$token" } build_channel_logos () { local -n ref=$1 [[ -n "$1" ]] || return 1 local url='http://120.87.12.38:8083/epg/api/custom/getAllChannel.json' local response response=$(curl "${CURL_ARGS[@]}" "$url") || return 1 [[ -n "$response" ]] || return 1 local channel local hwcode local icon while read -r channel; do hwcode=$(printf '%s' "$channel" | jq -r '.params.hwcode') || return 1 icon=$(printf '%s' "$channel" | jq -r '.icon') || return 1 [[ -n "$hwcode" && "$hwcode" != "null" ]] || continue [[ -n "$icon" && "$icon" != "null" ]] || continue ref["$hwcode"]="$icon" done < <(printf '%s' "$response" | jq -c '.channels[]') || return 1 } make_epg () { # Check dependencies ensure_curl_is_installed ensure_jq_is_installed # Default variables local output_file='epg.xml' local endpoint='http://120.87.12.38:8083/epg/api/page/biz_59417088.json' while [ $# -gt 0 ]; do case "$1" in -o | --output ) output_file="$2"; shift 2;; --endpoint ) endpoint="$2"; shift 2;; -I | --interface ) CURL_ARGS+=(--interface "$2"); shift 2;; -h | --help ) show_make_epg_usage; exit 0;; *) echo "Unknown option: $1"; exit 1;; esac done step "Request channel index" response=$(curl "${CURL_ARGS[@]}" "$endpoint") step "Create the EPG XML file" echo '' > "$output_file" echo '' >> "$output_file" echo "" >> "$output_file" while read -r item; do item_title=$(echo "$item" | jq -r '.itemTitle') item_type=$(echo "$item" | jq -r '.itemType') data_link=$(echo "$item" | jq -r '.dataLink') if [[ "$item_type" == "channel" ]]; then step "Process: $item_title" response=$(curl "${CURL_ARGS[@]}" "$data_link") channel_icon=$(echo "$response" | jq -r '.channel.icon') channel_title=$(echo "$response" | jq -r '.channel.title') hwcode=$(echo "$response" | jq -r '.channel.params.hwcode') echo " " >> "$output_file" echo " " >> "$output_file" echo " " >> "$output_file" echo " " >> "$output_file" while read -r schedule; do schedule_title=$(echo "$schedule" | jq -r '.title') schedule_starttime=$(echo "$schedule" | jq -r '.starttime') schedule_endtime=$(echo "$schedule" | jq -r '.endtime') echo " " >> "$output_file" echo " <![CDATA[$schedule_title]]>" >> "$output_file" echo " " >> "$output_file" done < <(echo "$response" | jq -c '.schedules[]') fi done < <(echo "$response" | jq -c '.areaDatas[].items[]') echo '' >> "$output_file" step "EPG built sucessfully!" exit 0 } make_playlist () { # Check dependencies ensure_curl_is_installed ensure_jq_is_installed ensure_openssl_is_installed # Default values local user_id='' local password='' local device_id='' local mac_address='' local ip_address='127.0.0.1' local udpxy_endpoint='http://127.0.0.1:4022' local output_file='playlist.M3U8' while [ $# -gt 0 ]; do case "$1" in -u | --user) user_id="$2"; shift 2;; -p | --password) password="$2"; shift 2;; -d | --device) device_id="$2"; shift 2;; --ip) ip_address="$2"; shift 2;; --mac) mac_address="$2"; shift 2;; --udpxy) udpxy_endpoint="$2"; shift 2;; -o | --output) output_file="$2"; shift 2;; -I | --interface) CURL_ARGS+=(--interface "$2"); shift 2;; -h | --help ) show_make_playlist_usage; exit 0;; *) echo "Unknown option: $1"; exit 1;; esac done # Validate user input if [[ -z "$user_id" ]]; then fatal "Requires an IPTV user ID with -u or --user" fi if [[ -z "$password" ]]; then fatal "Requires an IPTV password with -p or --password" fi if [[ -z "$device_id" ]]; then fatal "Requires a device ID with -d or --device" fi if [[ -z "$mac_address" ]]; then fatal "Requires a device MAC address with --mac" fi step "Authenticate" local host host=$(retrieve_endpoint "$user_id") || fatal 'Failed to retrieve endpoint' local encry_token encry_token=$(retrieve_encry_token "$host" "$user_id") || fatal 'Failed to retrieve encry token' local key key=$(printf "%-24s" "$password" | tr ' ' 0 | xxd -p) local authinfo authinfo=$(build_authinfo "$encry_token" "$user_id" "$device_id" "$ip_address" "$mac_address") || fatal 'Failed to build authinfo' authinfo=$(encrypt_authinfo "$authinfo" "$key") || fatal 'Failed to encrypt authinfo' local access_token access_token=$(retrieve_access_token "$host" "$user_id" "$authinfo") || fatal 'Failed to retrieve access token' step "Request channel logos" local -A logos build_channel_logos logos step "Request channels from the batch API" response=$(curl "${CURL_ARGS[@]}" \ --data '{"channelcodes":""}' \ --header 'Content-Type: application/json;charset=utf-8' \ --header "Authorization: $access_token" \ http://$host/EPG/interEpg/channellist/batch) step "Decode channels from the HTTP response" echo '#EXTM3U' > "$output_file" echo '#EXT-X-VERSION:3' >> "$output_file" echo "$response" | jq -c '.channellist[]' | while read -r channel; do channelcode=$(echo "$channel" | jq -r '.channelcode') channelname=$(echo "$channel" | jq -r '.channelname') channelurl=$(echo "$channel" | jq -r '.channelurl') IFS='|' read -ra channelurls <<< "$channelurl" step "Process: $channelname" pattern="://([^/]+)" if [[ "${channelurls[0]}" =~ $pattern ]]; then host=${BASH_REMATCH[1]} group=$(categorize_by_channel_name "$channelname") printf '#EXTINF:-1 tvg-id="%s" tvg-name="%s" tvg-logo="%s" group-name="%s",%s\n' \ "$channelcode" \ "$channelname" \ "${logos[$channelcode]-}" \ "$group" \ "$channelname" >> "$output_file" echo "$udpxy_endpoint/udp/$host" >> "$output_file" fi done step "Extract IPTV channels successfully!" exit 0 } decrypt_authinfo () { if [[ "$1" == '-h' || "$1" == "--help" ]]; then show_decrypt_authinfo_usage exit 0 fi local authinfo="$1" if [[ -z "$authinfo" ]]; then fatal "Requires authinfo data from /EPG/oauth/v2/token" fi authinfo=$(echo -n "$authinfo" | xxd -r -p) local loading=("Still working." "Still working.." "Still working...") local key for ((i=0; i<1000000; i++)); do key=$(printf "%06d" "$i") if (( i % 100 == 0 )); then echo -ne "\r\033[K${loading[i % 3]} $((i / 1000000 * 100))%" fi local result=$(echo -n "$authinfo" | \ openssl enc -d -des-ede3 -K "$(echo -n "${key}000000000000000000" | xxd -p -c 0)" 2>/dev/null | \ tr -d '\0') if echo -n "$result" | grep -q OTT; then IFS='$' read -ra components <<< "$result" echo -e "\n" echo "========================================" echo "Found key: $key" echo "========================================" echo "$result" echo "========================================" echo " random: ${components[0]}" echo " encry token: ${components[1]}" echo " user id: ${components[2]}" echo " device id: ${components[3]}" echo " ip address: ${components[4]}" echo " mac address: ${components[5]}" echo " reserved: ${components[6]}" echo " ott: ${components[7]}" exit 0 fi done fatal "Could not find a possible key." } ## ## Main ## while [ $# -gt 0 ]; do case "$1" in make:playlist) shift 1; make_playlist "$@";; make:epg) shift 1; make_epg "$@";; decrypt:authinfo) shift 1; decrypt_authinfo "$@";; -h | --help) show_usage; exit 0;; *) echo "Unknown command: $1"; exit 1;; esac done