mirror of
https://gitea.invidious.io/iv-org/youtube-utils.git
synced 2024-08-15 00:53:16 +00:00
602 lines
11 KiB
Bash
Executable file
602 lines
11 KiB
Bash
Executable file
#!/bin/sh
|
|
# shellcheck disable=SC2236,SC2237
|
|
#
|
|
# ^ Allow the use of `! -z` and `! [ -z]` as those are
|
|
# more intuitive than `-n`
|
|
|
|
|
|
#
|
|
# Globals
|
|
#
|
|
|
|
ALL_CLIENTS="web
|
|
web-embed
|
|
web-mobile
|
|
android
|
|
android-embed
|
|
apple-ios
|
|
tv-html5
|
|
tv-html5-embed"
|
|
|
|
|
|
#
|
|
# Utility functions
|
|
#
|
|
|
|
print_usage()
|
|
{
|
|
(
|
|
echo "Usage: yt-api-helper -i [-c <client>] [-e <endpoint>]"
|
|
echo "Usage: yt-api-helper -c <client> -e <endpoint> -d <data>"
|
|
) >&2
|
|
}
|
|
|
|
print_help()
|
|
{
|
|
print_usage
|
|
|
|
(
|
|
echo ""
|
|
echo "Options:"
|
|
echo " -c,--client Client to use. Mandatory in non-interactive mode."
|
|
echo " -d,--data Raw data to send to the API"
|
|
echo " -e,--endpoint Youtube endpoint to request."
|
|
echo " Mandatory in non-interactive mode"
|
|
echo " -h,--help Show this help"
|
|
echo " -i,--interactive Run in interactive mode"
|
|
echo " -o,--output Print output to file instead of stdout"
|
|
echo ""
|
|
echo " --debug Show what is sent to the API"
|
|
echo ""
|
|
) >&2
|
|
|
|
print_clients
|
|
print_endpoints
|
|
}
|
|
|
|
print_clients()
|
|
{
|
|
(
|
|
echo ""
|
|
echo "Available clients:"
|
|
|
|
for client in $ALL_CLIENTS; do
|
|
echo " - $client"
|
|
done
|
|
) >&2
|
|
}
|
|
|
|
print_endpoints()
|
|
{
|
|
(
|
|
echo ""
|
|
echo "Available endpoints:"
|
|
echo " - browse"
|
|
echo " - browse-continuation"
|
|
echo " - next"
|
|
echo " - next-continuation"
|
|
echo " - player"
|
|
echo " - search"
|
|
echo " - resolve"
|
|
) >&2
|
|
}
|
|
|
|
|
|
query_with_default()
|
|
{
|
|
prompt="$1"
|
|
default="$2"
|
|
|
|
printf "%s [%s]: " "$prompt" "$default" >&2
|
|
read -r data
|
|
|
|
if [ -z "$data" ]; then
|
|
echo "$default"
|
|
else
|
|
echo "$data"
|
|
fi
|
|
}
|
|
|
|
query_with_error()
|
|
{
|
|
prompt="$1"
|
|
error_message="$2"
|
|
|
|
printf "%s []: " "$prompt" >&2
|
|
read -r data
|
|
|
|
if [ -z "$data" ]; then
|
|
error_msg "$error_message"
|
|
exit 1
|
|
else
|
|
echo "$data"
|
|
fi
|
|
}
|
|
|
|
|
|
is_arg()
|
|
{
|
|
case $1 in
|
|
-c|--client) true;;
|
|
-d|--data) true;;
|
|
-e|--endpoint) true;;
|
|
-h|--help) true;;
|
|
-i|--interactive) true;;
|
|
-o|--output) true;;
|
|
--debug) true;;
|
|
*) false;;
|
|
esac
|
|
}
|
|
|
|
|
|
error_msg()
|
|
{
|
|
printf "Error: %s\n" "$1" >&2
|
|
}
|
|
|
|
|
|
#
|
|
# Client selection function
|
|
#
|
|
|
|
client_select()
|
|
{
|
|
# Default API key, used by most clients
|
|
apikey="AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8"
|
|
|
|
# Default user-agent
|
|
user_agent="Mozilla/5.0 (Windows NT 10.0; rv:78.0) Gecko/20100101 Firefox/78.0"
|
|
|
|
# Reset values, in case this function is used multiple times
|
|
client_name=""
|
|
client_vers=""
|
|
|
|
client_extra_device_make=""
|
|
client_extra_device_model=""
|
|
client_extra_os_name=""
|
|
client_extra_os_vers=""
|
|
client_extra_platform=""
|
|
client_extra_form_factor=""
|
|
|
|
|
|
case "$1" in
|
|
web)
|
|
client_name="WEB"
|
|
client_vers="2.20230217.01.00"
|
|
;;
|
|
|
|
web-embed)
|
|
client_name="WEB_EMBEDDED_PLAYER"
|
|
client_vers="1.20230217.01.0"
|
|
;;
|
|
|
|
web-mobile)
|
|
client_name="MWEB"
|
|
client_vers="2.20230216.06.00"
|
|
;;
|
|
|
|
android)
|
|
apikey="AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39w"
|
|
client_name="ANDROID"
|
|
client_vers="17.31.35"
|
|
;;
|
|
|
|
android-embed)
|
|
client_name="ANDROID_EMBEDDED_PLAYER"
|
|
client_vers="17.31.35"
|
|
;;
|
|
|
|
apple-ios)
|
|
apikey="AIzaSyB-63vPrdThhKuerbB2N_l7Kwwcxj6yUAc"
|
|
client_name="IOS"
|
|
client_vers="17.31.4"
|
|
|
|
client_extra_device_make="Apple"
|
|
client_extra_device_model="iPhone11,8"
|
|
client_extra_os_vers="15.2.0"
|
|
|
|
user_agent="com.google.ios.youtube/17.31.4 (iPhone11,8; U; CPU iOS 15_2 like Mac OS X; en_GB)"
|
|
;;
|
|
|
|
tv-html5)
|
|
client_name="TVHTML5"
|
|
client_vers="7.20220325"
|
|
;;
|
|
|
|
tv-html5-embed)
|
|
client_name="TVHTML5_SIMPLY_EMBEDDED_PLAYER"
|
|
client_vers="2.0"
|
|
screen="EMBED"
|
|
;;
|
|
|
|
*)
|
|
error_msg "Unknown client '$client_option'"
|
|
print_clients
|
|
exit 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
|
|
#
|
|
# Endpoint selection function
|
|
#
|
|
|
|
endpoint_select()
|
|
{
|
|
case "$1" in
|
|
browse)
|
|
endpoint="youtubei/v1/browse"
|
|
|
|
if [ "$interactive" = true ]; then
|
|
browse_id=$(query_with_default "Enter browse ID" "UCXuqSBlHAE6Xw-yeJA0Tunw")
|
|
endpoint_data="\"browseId\":\"${browse_id}\""
|
|
fi
|
|
;;
|
|
|
|
browse-cont*|browse-tok*)
|
|
endpoint="youtubei/v1/browse"
|
|
|
|
if [ "$interactive" = true ]; then
|
|
token=$(query_with_error "Enter continuation token" "token required")
|
|
endpoint_data="\"continuation\":\"${token}\""
|
|
fi
|
|
;;
|
|
|
|
player|next)
|
|
endpoint="youtubei/v1/$endpoint_option"
|
|
|
|
if [ "$interactive" = true ]; then
|
|
vid=$(query_with_default "Enter video ID" "dQw4w9WgXcQ")
|
|
endpoint_data="\"videoId\":\"${vid}\""
|
|
|
|
fi
|
|
;;
|
|
|
|
next-cont*|next-tok*)
|
|
endpoint="youtubei/v1/next"
|
|
|
|
if [ "$interactive" = true ]; then
|
|
token=$(query_with_error "Enter continuation token" "token required")
|
|
endpoint_data="\"continuation\":\"${token}\""
|
|
fi
|
|
;;
|
|
|
|
search)
|
|
endpoint="youtubei/v1/search"
|
|
|
|
if [ "$interactive" = true ]; then
|
|
# Get search query, and escape backslashes and double quotes
|
|
query=$(
|
|
query_with_error "Enter your search query" "search term required" |
|
|
sed -e 's/\\/\\\\/g' -e 's/"/\\"/g'
|
|
)
|
|
endpoint_data="\"query\":\"${query}\""
|
|
fi
|
|
;;
|
|
|
|
resolve)
|
|
endpoint="youtubei/v1/navigation/resolve_url"
|
|
|
|
if [ "$interactive" = true ]; then
|
|
url=$(query_with_error "Enter URL" "URL required")
|
|
endpoint_data="\"url\":\"${url}\""
|
|
fi
|
|
;;
|
|
|
|
*)
|
|
error_msg "Unknown endpoint '$1'"
|
|
print_endpoints
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
|
|
# Interactively request additional parameters for the supported endpoints
|
|
if [ "$interactive" = true ]
|
|
then
|
|
case "$1" in
|
|
|
|
browse|player|search)
|
|
params=$(query_with_default "Enter optional parameters (base64-encoded protobuf)" "")
|
|
|
|
if [ ! -z "$params" ]; then
|
|
endpoint_data="${endpoint_data},\"params\":\"${params}\""
|
|
fi
|
|
;;
|
|
esac
|
|
fi
|
|
}
|
|
|
|
|
|
#
|
|
# Function to craft the client data
|
|
#
|
|
|
|
make_request_data()
|
|
{
|
|
client="\"hl\":\"${hl}\",\"gl\":\"${gl}\""
|
|
|
|
client="${client},\"deviceMake\":\"${client_extra_device_make}\""
|
|
client="${client},\"deviceModel\":\"${client_extra_device_model}\""
|
|
|
|
if ! [ -z "$screen" ]; then
|
|
client="${client},\"clientScreen\":\"${screen}\""
|
|
fi
|
|
|
|
client="${client},\"clientName\":\"${client_name}\""
|
|
client="${client},\"clientVersion\":\"${client_vers}\""
|
|
|
|
if ! [ -z "$client_extra_os_name" ]; then
|
|
client="${client},\"osName\":\"${client_extra_os_name}\""
|
|
fi
|
|
|
|
if ! [ -z "$client_extra_os_vers" ]; then
|
|
client="${client},\"osVersion\":\"${client_extra_os_vers}\""
|
|
fi
|
|
|
|
if ! [ -z "$client_extra_platform" ]; then
|
|
client="${client},\"platform\":\"${client_extra_platform}\""
|
|
fi
|
|
|
|
if ! [ -z "$client_extra_form_factor" ]; then
|
|
client="${client},\"clientFormFactor\":\"${client_extra_form_factor}\""
|
|
fi
|
|
|
|
|
|
data="{\"context\":{\"client\":{$client}},$endpoint_data}"
|
|
|
|
# Basic debug
|
|
if [ "$debug" = true ]; then
|
|
if command -v jq >&2 >/dev/null; then
|
|
printf "\nSending: %s\n\n" "$data" | jq . >&2
|
|
else
|
|
printf "\nSending: %s\n\n" "$data" | sed 's/{/{\n/g; s/}/\n}/g; s/,/,\n/g' >&2
|
|
fi
|
|
fi
|
|
|
|
# Return data
|
|
printf "{\"context\":{\"client\":{%s}},%s}\n" "$client" "$endpoint_data"
|
|
}
|
|
|
|
|
|
#
|
|
# Final command - send request to Youtube
|
|
#
|
|
|
|
send_request()
|
|
{
|
|
request_data="$1"
|
|
output_file="$2"
|
|
|
|
url="https://www.youtube.com/${endpoint}?key=${apikey}"
|
|
|
|
# Headers
|
|
hdr_ct='Content-Type: application/json; charset=utf-8'
|
|
hdr_ua="User-Agent: ${user_agent}"
|
|
|
|
# Run!
|
|
temp_dl=_curl_$(date '+%s')
|
|
|
|
curl --compressed -H "$hdr_ct" -H "$hdr_ua" --data "$request_data" "$url" | \
|
|
sed -E '
|
|
/^\s+"(clickT|t)rackingParams.+,$/d
|
|
s/,?\n\s+"(clickT|t)rackingParams.+$//
|
|
' > "$temp_dl"
|
|
|
|
# Print to STDOUT if no output file was given
|
|
if [ -z "$output_file" ]; then
|
|
cat "$temp_dl"
|
|
rm "$temp_dl"
|
|
else
|
|
mv -- "$temp_dl" "$output_file"
|
|
fi
|
|
}
|
|
|
|
|
|
#
|
|
# Parameters init
|
|
#
|
|
|
|
interactive=false
|
|
debug=false
|
|
|
|
client_option=""
|
|
endpoint_option=""
|
|
|
|
data=""
|
|
|
|
|
|
#
|
|
# Parse arguments
|
|
#
|
|
|
|
while :; do
|
|
# Exit if no more arguments to parse
|
|
if [ $# -eq 0 ]; then break; fi
|
|
|
|
case $1 in
|
|
-c|--client)
|
|
shift
|
|
|
|
if [ $# -eq 0 ] || is_arg "$1"; then
|
|
error_msg "missing argument after -c/--client"
|
|
print_usage
|
|
exit 2
|
|
fi
|
|
|
|
client_option=$1
|
|
;;
|
|
|
|
-d|--data)
|
|
shift
|
|
|
|
if [ $# -eq 0 ] || is_arg "$1"; then
|
|
error_msg "missing argument after -d/--data"
|
|
print_usage
|
|
exit 2
|
|
fi
|
|
|
|
data=$1
|
|
;;
|
|
|
|
-e|--endpoint)
|
|
shift
|
|
|
|
if [ $# -eq 0 ] || is_arg "$1"; then
|
|
error_msg "missing argument after -e/--endpoint"
|
|
print_usage
|
|
exit 2
|
|
fi
|
|
|
|
endpoint_option=$1
|
|
;;
|
|
|
|
-h|--help)
|
|
print_help
|
|
exit 0
|
|
;;
|
|
|
|
-i|--interactive)
|
|
interactive=true
|
|
;;
|
|
|
|
-o|--output)
|
|
shift
|
|
|
|
if [ $# -eq 0 ] || is_arg "$1"; then
|
|
error_msg "missing argument after -o/--output"
|
|
print_usage
|
|
exit 2
|
|
fi
|
|
|
|
output="$1"
|
|
;;
|
|
|
|
--debug)
|
|
debug=true
|
|
;;
|
|
|
|
*)
|
|
error_msg "unknown argument '$1'"
|
|
print_usage
|
|
exit 2
|
|
;;
|
|
esac
|
|
|
|
shift
|
|
done
|
|
|
|
|
|
#
|
|
# Input validation
|
|
#
|
|
|
|
if [ ! -z "$data" ]; then
|
|
# Can't pass data in interactive mode
|
|
if [ "$interactive" = true ]; then
|
|
error_msg "-d/--data can't be used with -i/--interactive"
|
|
print_usage
|
|
exit 2
|
|
fi
|
|
|
|
# In non-interactive mode, we still need to pass a client
|
|
# so the right API key is passed as a URL parameter
|
|
if [ -z "$client_option" ]; then
|
|
error_msg "-c/--client is required to select an API key"
|
|
print_usage
|
|
exit 2
|
|
fi
|
|
|
|
# Endpoint must be given if non-interactive mode
|
|
if [ -z "$endpoint_option" ]; then
|
|
error_msg "In non-interactive mode, an endpoint must be passed with -e/--endpoint"
|
|
print_usage
|
|
exit 2
|
|
fi
|
|
fi
|
|
|
|
if [ -z "$data" ] && [ "$interactive" = false ]; then
|
|
# Data must be given if non-interactive mode
|
|
error_msg "In non-interactive mode, data must be passed with -d/--data"
|
|
print_usage
|
|
exit 2
|
|
fi
|
|
|
|
if [ -z "$output" ] && [ "$interactive" = true ]; then
|
|
confirm=$(query_with_default "\nIt's recommended to use --output in interactive mode.\nContinue?" "No")
|
|
|
|
case $confirm in
|
|
[Yy]|[Yy][Ee][Ss]) ;;
|
|
*) exit 0;;
|
|
esac
|
|
fi
|
|
|
|
|
|
#
|
|
# Required parameters. Ask if not provided
|
|
#
|
|
|
|
if [ -z "$client_option" ]; then
|
|
if [ "$interactive" = true ]; then
|
|
print_clients
|
|
client_option=$(query_with_default "\nEnter a client to use" "web")
|
|
else
|
|
exit 2
|
|
fi
|
|
fi
|
|
|
|
if [ -z "$endpoint_option" ]; then
|
|
if [ "$interactive" = true ]; then
|
|
print_endpoints
|
|
endpoint_option=$(query_with_default "\nEnter an endpoint to request" "")
|
|
else
|
|
exit 2
|
|
fi
|
|
fi
|
|
|
|
# new line
|
|
echo >&2
|
|
|
|
# Interactive language/region selection
|
|
if [ "$interactive" = true ]; then
|
|
hl=$(query_with_default "Enter content language (hl)" "en")
|
|
gl=$(query_with_default "Enter content region (gl)" "US")
|
|
fi
|
|
|
|
|
|
#
|
|
# Run the request
|
|
#
|
|
|
|
# Only ran once (this requests some additional data in interactive mode)
|
|
endpoint_select "$endpoint_option"
|
|
|
|
if [ "$client_option" = "all" ]; then
|
|
# Run the client selector (get client-specific infos) and request for each client
|
|
if [ -z "$output" ]; then
|
|
printf "\nAll clients requested, response will be written to output file anyway\n" >&2
|
|
output=response-$endpoint_option.$(date '+%s')
|
|
else
|
|
output=$(basename "$output" ".json")
|
|
fi
|
|
|
|
for client_name in $ALL_CLIENTS; do
|
|
client_select "$client_name"
|
|
|
|
if [ "$interactive" = true ]; then
|
|
data=$(make_request_data)
|
|
fi
|
|
|
|
send_request "$data" "$output.$client_name.json"
|
|
done
|
|
else
|
|
# Run the client selection and request only once
|
|
client_select "$client_option"
|
|
|
|
if [ "$interactive" = true ]; then
|
|
data=$(make_request_data);
|
|
fi
|
|
|
|
send_request "$data" "$output"
|
|
fi
|