#!/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="[ ]" args="[ ]" 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 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 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() { op=0 handshake='{"v":1,"client_id":"'$presence_client_id'"}' datalen=${#handshake} for i in $(seq 0 3); do byte=$(((op >> (i * 8)) & 255)) printf "\\$(printf "%03o" "$byte")" done for i in $(seq 0 3); do byte=$(((datalen >> (i * 8)) & 255)) printf "\\$(printf "%03o" "$byte")" done printf "%s" "$handshake" op=1 datalen=${#1} for i in $(seq 0 3); do byte=$(((op >> (i * 8)) & 255)) printf "\\$(printf "%03o" "$byte")" done for i in $(seq 0 3); do byte=$(((datalen >> (i * 8)) & 255)) printf "\\$(printf "%03o" "$byte")" done printf "%s" "$1" } update_rich_presence() { pkill -f "nc -U $discord_ipc" > /dev/null sleep 2 title=$1 id=$2 start=$(date +%s) dur=$(printf '{ "command": ["get_property", "duration"] }\n' | socat - "$socket" | sed -nE 's|.*data":([^.]*).*|\1|p') 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"}}}, "nonce": "1"}' #with buttons, but this MF is not working -- #payload='{"cmd": "SET_ACTIVITY", "args": {"activity": {"details": "'$(printf "%s" "$title" | sed 's|[^-]*$||;s|-$||;s| $||;s|^ ||')'", "state": "'$(printf "%s" "$title" | sed 's_.* - __;s| $||')'", "timestamps": {"start": '"$(date +%s)"'}, "assets": {"large_image": "https://i.ytimg.com/vi/'$id'/mqdefault.jpg","large_text":"Checkmate Premium Users","buttons": [{"label":"Listen Here", "url": "https://www.youtube.com/watch?v='$id'"}]}}}, "nonce": "1"}' set_activity "$payload" | nc -U "$discord_ipc" } 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.22.51" extra_up=$((random_no >= 2)) 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" #self explainatory get_song_lyrics "$id" & #update the discord rich presence, requires client id update_rich_presence "$title" "$id" & #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 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.20231010.05.00"}}' >"$logdir/context" 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|.*([^<]*)<.*|\1|p' | sed 's|&|\&|g') res=$(printf '%s\t%s' "$title" "$id") fi #extracting your cookies so that the song list are according to your taste get_cookies 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() { bemenu --fn 'IBM Plex Sans 15' -i -c -W 0.5 -B 3 -p "$1" -l 25 -I "${2:-0}" -P ">>" } logdir="/tmp/${0##*/}" socket="$logdir/${0##*/}-mpvsocket" discord_ipc="${XDG_RUNTIME_DIR}/discord-ipc-0" presence_client_id="1165951947245879316" base_url="https://music.youtube.com" [ -d "$logdir" ] || mkdir "$logdir" cookie="$logdir/cookies" agent="Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/116.0" #call this script by script-name "function_name" "query" [ -z "$1" ] && help "$@" $1 "$2" "$3"