Files
repos_scripts/yt-music
2024-03-01 13:05:33 +05:30

329 lines
13 KiB
Bash
Executable File

#!/bin/sh
#shellcheck disable=SC2030,SC2031,SC2059
help() {
case $1 in
search*)
name="search_play"
args='"search_query"'
description="this function first searches the $args, then opens the menu(fzf,[d|be]menu,[t|r|w]ofi) with the results,then calls the play function to play the music"
;;
play)
name="play"
args='"valid_youtube_url_or_id"'
description="immediately plays the $args in mpv(audio only)"
;;
loop)
name="loop"
args='"print"'
description="this function runs in a loop to play the next music if the current music is successfully finished
this function should be put as background process or put it in startup
passing print argument will show the progress to stdout"
;;
play*)
name="play_next"
args='"menu"'
description="immediately plays the next song stored in $logdir/next file in mpv if argument is empty
passing $args argument will show the $args(fzf,[d|be]menu,[t|r|w]ofi) for selecting what should be played"
;;
*)
name="[ <function_name> ]"
args="[ <function_arg> ]"
description="
<function_name> <function_arg> <function_description>
search_play search_query searches first then plays the music in mpv
play youtube_url plays the music in mpv
play_next menu(optional) plays the next music in $logdir/next file
loop print(optional) plays the next music after the current is finished (run it as background process)
tip:
type ${0##*/} help <function_name> to get individual help
"
;;
esac
while read -r line; do
printf "%s\n" "$line"
done <<-EOF
Usage :
${0##*/} $name $args
Description :
$description
EOF
exit 0
}
cleanup_shit() {
pkill -f "nc -U $discord_ipc" >/dev/null
pkill -f "tail -f $presence" >/dev/null
rm -rdf "$logdir"
exit 0
}
get_cookies() {
for i in $(sqlite3 "$HOME/.config/google-chrome/Default/Cookies" "SELECT name,REPLACE(base64(SUBSTR(encrypted_value,4)),CHAR(10),'') FROM cookies WHERE host_key='.youtube.com';"); do
printf "%s=%s; " "$(printf '%s' "$i" | cut -d'|' -f1)" "$(printf '%s' "$i" | cut -d'|' -f2 | base64 -d | openssl enc -d -aes-128-cbc -K fd621fe5a2b402539dfa147ca9272778 -iv 20202020202020202020202020202020)"
done >"$cookie"
}
get_data() {
grep -q "SAPISID" "$logdir/cookies" 2>/dev/null && sapisid_header="Authorization: SAPISIDHASH $(printf '%s_%s' "$(date +%s)" "$(printf '%s %s %s' "$(date +%s)" "$(sed -nE 's|.*SAPISID=([^;]*);.*|\1|p' "$logdir/cookies")" "$base_url" | sha1sum | cut -d' ' -f1)")" || sapisid_header=""
curl -X POST -A "${3:-$agent}" -s "$base_url/youtubei/v1/$1?key=${4:-AIzaSyC9XL3ZjWddXya6X74dJoCTL-WEYFDNX30}&prettyPrint=false" -H "content-type:application/json" -d "$2" -b "$(cat "$cookie")" -e "$base_url" -H "$sapisid_header"
}
get_music_list() {
json_next="{
\"enablePersistentPlaylistPanel\": true,
\"tunerSettingValue\": \"AUTOMIX_SETTING_NORMAL\",
\"playlistId\": \"RDAMVM$(cat "$logdir/start")\",
\"index\": $(cat "$logdir/counter"),
\"params\": \"wAEB\",
$([ -e "$logdir/continue_token" ] && cat "$logdir/continue_token")
\"isAudioOnly\": true,
\"context\": $(cat "$logdir/context")
}"
next_data=$(get_data "next" "$json_next" | sed 's/playlistPanelVideoRenderer/\n/g;s/hasPersistentPlaylistPanel/\n/g' | sed -nE 's|.*text":"(.*)"}.*longBylineText":\{"runs":\[\{"text":"([^"]*)","navigationEndpoint.*videoId":"([^"]*)".*|\1 - \2\t\3|p;s|.*nextRadioContinuationData":\{([^,]*).*|\1,|p')
printf '%s' "$next_data" | sed -e "$(cut -f2 "/tmp/yt-music/next" | sed 's|^|/|g;s|$|/d|g')" -e '/"continuation"/d' >>"$logdir/next"
printf '%s' "$next_data" | sed -n '/"continuation"/p' >"$logdir/continue_token"
}
get_song_lyrics() {
json_next="{
\"enablePersistentPlaylistPanel\": true,
\"tunerSettingValue\": \"AUTOMIX_SETTING_NORMAL\",
\"playlistId\": \"RDAMVM$(cat "$logdir/start")\",
\"index\": $(cat "$logdir/counter"),
\"videoId\": \"$1\",
\"isAudioOnly\": true,
\"context\": $(cat "$logdir/context")
}"
browseId=$(get_data "next" "$json_next" | sed -nE 's|.*"browseId":"(MPLYt[^"]*)".*TRACK_LYRICS.*|\1|p')
if [ -n "$browseId" ]; then
json_lyrics="{
\"context\": {
\"client\": {
\"clientName\": \"ANDROID_MUSIC\",
\"clientVersion\": \"$andy_yt_ver\",
\"androidSdkVersion\": $((random_no + extra_up + 29)),
\"userAgent\": \"$droid_agent\",
\"hl\": \"en\",
\"timeZone\": \"UTC\",
\"utcOffsetMinutes\": 0
}
},
\"browseId\": \"$browseId\"
}"
get_data "browse" "$json_lyrics" "$droid_agent" "AIzaSyAOghZGza2MQSZkY_zfZ370N-PUdXEo8AI" | sed 's/metadata"/\n/g' | sed -nE 's|.*lyricLine":"([^"]*)","cueRange".*"endTimeMilliseconds":"([^"]*)".*|\2\t\1|p' >"$logdir/lyrics"
fi
}
loop() {
#this function does exactly what it says, it should run in the background
#it plays next song after the current song get played completely,it does nothing until u run the search_play function then this code kicks in
trap cleanup_shit INT HUP TERM
socat - "UNIX-CONNECT:$socket" | while read -r event; do
#look for eof event
if printf "%s" "$event" | grep -q "end-file.*eof"; then
i=$(cat "$logdir/counter")
: $((i += 1))
pgrep -f "$socket" >/dev/null || continue
[ -n "$(cat "$logdir/next")" ]
play "$(sed -n "$((i += 1))p" "$logdir/next")" "$1"
printf '%s' "$i" >"$logdir/counter"
tail -1 "$logdir/next" | grep -q "$(cut -d'>' -f2 <"$logdir/current")" && get_music_list
fi
done
cleanup_shit
}
#discord rich presence written in a nut-shell code
set_activity() {
#length of payload
len=${#1}
#outputs opcode in little endian encoding
printf "\\001\\000\\000\\000"
#loop for length to encode in little endian encoding
for i in 0 8 16 24; do
len=$((len >> i))
#shellcheck disable=SC2059
printf "\\$(printf "%03o" "$len")"
done
#print the payload that need to be sent
printf "%s" "$1"
}
start_rich_presence() {
rm -f "$handshook" > /dev/null
tail -f "$presence" | nc -U "$discord_ipc" > "/tmp/ipclog"
}
update_rich_presence() {
title=$1
id=$2
dur=$3
start=$(date +%s)
payload='{"cmd": "SET_ACTIVITY", "args": {"activity": {"details": "'$(printf "%s" "$title" | sed 's|[^-]*$||;s|-$||;s| $||;s|^ ||')'", "state": "'$(printf "%s" "$title" | sed 's_.* - __;s| $||')'", "timestamps": {"start": '"$start"',"end": '"$((start + dur))"'}, "assets": {"large_image": "https://i.ytimg.com/vi/'$id'/mqdefault.jpg","large_text":"Checkmate Premium Users"},"buttons": [{"label":"Listen Song", "url": "https://music.youtube.com/watch?v='$id'"}]},"pid":786}, "nonce": "1"}' #pid needs to be set and constant so discord knows which application to update
if [ -e "$handshook" ];then
set_activity "$payload" > "$presence"
else
{
#handshake
handshake='{"v":1,"client_id":"'$presence_client_id'"}'
# handshake with opcode
printf "\\000\\000\\000\\000\\$(printf "%03o" "${#handshake}")\\000\\000\\000%s" "$handshake"
#wait for response which comes within 1 second(max 2 seconds)
sleep 2
set_activity "$payload"
} > "$presence"
touch "$handshook"
fi
}
play() {
#this function does all the heavy lifting of extracting url from given videoId
#it's also callable, u can use this function to play ur custom youtube URLs
title=$(printf "%s" "$1" | cut -f1)
id=$(printf "%s" "$1" | cut -f2 | cut -d"=" -f2 | cut -d"/" -f4 | cut -d'&' -f1)
[ -z "$id" ] && printf "[ youtube ] Invalid link\n" && exit 1
#get song's audio url
random_no=$(head /dev/urandom | tr -dc '0-4' | cut -c1)
andy_yt_ver="6.26.50"
extra_up=$((random_no > 1))
droid_agent="com.google.android.apps.youtube.music/$andy_yt_ver (Linux; U; Android 1$random_no) gzip"
json="{
\"context\": {
\"client\": {
\"clientName\": \"ANDROID_MUSIC\",
\"clientVersion\": \"$andy_yt_ver\",
\"androidSdkVersion\": $((random_no + extra_up + 29)),
\"userAgent\": \"$droid_agent\",
\"hl\": \"en\",
\"timeZone\": \"UTC\",
\"utcOffsetMinutes\": 0
}
},
\"videoId\": \"$id\",
\"playbackContext\": {
\"contentPlaybackContext\": {
\"html5Preference\": \"HTML5_PREF_WANTS\"
}
},
\"contentCheckOk\": true,
\"racyCheckOk\": true
}"
audio_url=$(get_data "player" "$json" "$droid_agent" "AIzaSyAOghZGza2MQSZkY_zfZ370N-PUdXEo8AI" | sed -nE 's_.*itag":251,"url":"([^"]*)".*_\1_p')
[ -z "$audio_url" ] && return 0
if [ -n "$2" ]; then
printf "Name >> %s\n" "$title"
printf "videoID >> %s\n" "$id"
printf "Audio URL >> %s\n" "$audio_url"
fi
curl -s "https://i.ytimg.com/vi/$id/hqdefault.jpg" -o - | convert - -crop 270x270+105+45 "$logdir/default.jpg" && notify-send -h "string:x-canonical-private-synchronous:${0##*/}" -i "$logdir/default.jpg" "Now Playing" "$title" -t 5000
pgrep -f "$socket" >/dev/null || (setsid -f mpv --really-quiet --input-ipc-server="$socket" --idle --quiet >/dev/null && sleep 1)
printf '{"command":["loadfile","%s","replace"]}\n' "$audio_url" | socat - "$socket"
printf "currently playing : %s >%s\n" "$title" "$id" >"$logdir/current"
#required for discord rich presence to set end timestamp
duration=$(printf '%s' "$audio_url" | sed -nE 's|.*&dur=([^\.]*).*|\1|p')
#self explainatory
get_song_lyrics "$id" &
#start the presence
pgrep -f "nc -U $discord_ipc" > /dev/null || start_rich_presence &
#update the discord rich presence, requires client id
update_rich_presence "$title" "$id" "$duration" &
#next songs data
[ -n "$3" ] && get_music_list &
pgrep -f "${0##*/} loop" >/dev/null || setsid -f "$0" loop
}
search_play() {
#run this if u r starting the script first time like this
#call this by "script-name" "search_play" [ search_query | youtube_id by prefixing with ID:<youtube_id> | youtube url ]
[ -z "$1" ] && query=$(: | menu "Yt-music [Search]:" "" "60") || query="$1"
[ -z "$query" ] && notify-send "Err.. Search query empty" -u critical -h "string:x-canonical-private-synchronous:${0##*/}" && exit 1
#storing context
printf '{"client":{"clientName":"WEB_REMIX","clientVersion":"1.20240226.01.00"}}' >"$logdir/context"
#extracting your cookies so that the song list are according to your taste
get_cookies
if ! (printf '%s' "$query" | grep -q 'https:' || printf '%s' "$query" | grep -q 'ID:'); then
#json for song search
json_search="{
\"context\" : $(cat "$logdir/context"),
\"query\": \"$query\",
\"params\": \"EgWKAQIIAWoKEAMQBBAJEAoQBQ%3D%3D\"
}"
res=$(get_data "search" "$json_search" "$agent" | sed 's/watchEndpoint"/\n/g' | sed -nE 's_.*videoId":"([^"]*)",.*label":"Play ([^"]*)".*_\2\t\1_p' | menu "Yt-music [Play]:")
else
id=$(printf '%s' "$query" | cut -d':' -f2 | cut -d"=" -f2 | cut -d"/" -f4 | cut -d'&' -f1)
title=$(curl -s "https://www.youtube.com/oembed?url=http://www.youtube.com/watch?v=$id&format=xml" | sed -nE 's|.*<title>([^<]*)<.*|\1|p' | sed 's|&amp;|\&|g')
res=$(printf '%s\t%s' "$title" "$id")
fi
printf '' >"$logdir/next"
printf "%s\n" "$res" >>"$logdir/next"
printf "%s" "$res" | cut -f2 >"$logdir/start"
printf "0" >"$logdir/counter"
rm -f "$logdir/continue_token"
[ -z "$res" ] || play "$res" "verbose" "1"
}
play_next() {
#call this by script-name "play_next" for playing next song immediately
#or add "menu" after "play_next" to show menu for selecting and playing next song immediately
#like this script-name "play_next" "menu"
pgrep -f "$socket" || return 0
i=$(cat "$logdir/counter")
if [ -z "$1" ]; then
: $((i += 1))
play "$(sed -n "$((i += 1))p" "$logdir/next")" "$1"
else
notify-send -h "string:x-canonical-private-synchronous:${0##*/}" -i "$logdir/default.jpg" "$(cut -d">" -f1 "$logdir/current" | tr ':' '\n')"
next=$(nl -n'ln' -v0 "$logdir/next" | sed "s/^$i /& /" | menu "YT-music [play-next]: " "$i")
[ -z "$next" ] && return 0
i=$(printf '%s' "$next" | sed 's///g' | cut -f1 | tr -d ' ')
play "$(printf '%s' "$next" | cut -f2-)" "verbose"
fi
printf '%s' "$i" >"$logdir/counter"
tail -1 "$logdir/next" | grep -q "$(cut -d'>' -f2 <"$logdir/current")" && get_music_list
}
menu() {
if command -v bemenu >/dev/null; then
bemenu --fn 'IBM Plex Sans 15' -i -c -W 0.5 -B 3 -p "$1" -l 25 -I "${2:-0}" -P ">>" --bdr="#$GLOBAL_ACCENT" --tf="#$GLOBAL_ACCENT" --hf="#$GLOBAL_ACCENT"
else
fzf --prompt="$1" --height=25 --reverse --border=horizontal --header="${2:-0}" --marker=">>"
fi
}
logdir="/tmp/${0##*/}"
socket="$logdir/${0##*/}-mpvsocket"
discord_ipc="${XDG_RUNTIME_DIR}/discord-ipc-0"
handshook="$logdir/handshook"
presence="$logdir/${0##*/}-presence"
presence_client_id="1165951947245879316"
base_url="https://music.youtube.com"
[ -d "$logdir" ] || mkdir "$logdir"
[ -p "$presence" ] || mkfifo "$presence"
cookie="$logdir/cookies"
agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36,gzip(gfe)"
#call this script by script-name "function_name" "query"
[ -z "$1" ] && help "$@"
$1 "$2" "$3"