#!/usr/bin/env bash # Requires: # * curl # * fzf | rofi # * jq # * youtube-dl # * hq | pup (recomended, although optional) rofi="auto" ttyecho(){ # Same as echo but always to tty echo "$@" > /dev/tty } urlencodespecial() { # urlencode old_lc_collate=$LC_COLLATE LC_COLLATE=C local length="${#1}" for (( i = 0; i < length; i++ )); do local c="${1:i:1}" case "$c" in [a-zA-Z0-9.~_-]) printf "$c" ;; ' ') printf "+" ;; *) printf '%%%02X' "'$c" ;; esac done LC_COLLATE=$old_lc_collate } forceread(){ # Read but always from tty and will keep asking until a non-empty value is # given # First param is given is prompt local answer="" local first="true" while [ -z "$answer" ]; do [ "$first" == "true" ] || ttyecho "Please provide an answer" first="false" ttyecho -n "$1: " read answer < /dev/tty done echo "$answer" } useRofi(){ [ "$rofi" = "yes" ] && return 0 local isxclient=$( readlink /dev/fd/2 | grep -q 'tty' && [[ -n $DISPLAY ]] ; echo $? ) if [[ ! -t 2 || $isxclient == "0" ]]; then return 0 else return 1 fi } getSearchTerm(){ if useRofi; then # Rofi can't reload the suggestions so just display a search box echo "" | rofi -dmenu -lines 0 else # Uses fzf to get a search term # It will filter suggestions using youtube's autocomplete export -f getSearchSuggestions export -f urlencodespecial echo "" | fzf --bind 'change:reload:bash -c "getSearchSuggestions {q}" || true' \ --header="Search Suggestions" \ --phony \ --height=50% --layout=reverse fi } getSearchSuggestions(){ # Takes a query and outputs the search suggestions that youtube makes query="$(urlencodespecial "$1")" curl "https://clients1.google.com/complete/search?client=youtube&hl=en&gl=gb&gs_rn=64&gs_ri=youtube&ds=yt&cp=5&gs_id=4e&q=$query&xhr=t&xssi=t" | tail -n 1 | jq -r '.[1][][0]' } performSearch(){ # Returns the json object yt gives us local raw="$1" local url="https://www.youtube.com/results?search_query=$(urlencodespecial "$raw")" if type -p hq > /dev/null; then curl -s "$url" | hq script data | grep 'ytInitialData' | head -n 1 | grep -o '{.*}' elif type -p pup > /dev/null; then curl -s "$url" | pup script | grep 'ytInitialData' | head -n 1 | grep -o '{.*}' else curl -s "$url" | sed -e 's/]*>/\n/g' -e 's/<\/script>/\n/g' | grep 'ytInitialData' | head -n 1 | grep -o '{.*}' fi } extractVideoInfo(){ jq '..|.videoRenderer? | values' | jq -r '{title: .title.runs[0].text, channel: .longBylineText.runs[0].text, views: .shortViewCountText.simpleText, time: .lengthText.simpleText, age: .publishedTimeText.simpleText, video: .videoId }' } chooseVideo(){ if useRofi; then ( jq -r '[ .title, .channel, .views, .time, .age, .video ] | @tsv') | column -t -s" " | sed $'s/ \([^ ]\)/\u00a0\\1/g' | rofi -dmenu else (echo -e "Title\tChannel\tViews\tTime\tAge\tId" jq -r '[ .title, .channel, .views, .time, .age, .video ] | @tsv') | column -t -s" " | sed $'s/ \([^ ]\)/\u00a0\\1/g' | fzf --header-lines="1" --with-nth=1,2,3,4,5 --delimiter=$'\u00a0' fi } chooseQuality(){ if [ -n "$1" ]; then videoId="$1" else videoId="$(cat - | awk -F $'\u00a0' '{print $6}')" fi if type -p yt-dlp > /dev/null; then choices=" bb Best of Both $(yt-dlp "$videoID" -F --compat-options list-formats | sed -n '/format code/,$ p' | sed 1d)" else # 249 webm audio only tiny 50k , webm_dash container, opus @ 50k (48000Hz), 49.85MiB # 250 webm audio only tiny 60k , webm_dash container, opus @ 60k (48000Hz), 59.86MiB # 251 webm audio only tiny 115k , webm_dash container, opus @115k (48000Hz), 113.94MiB # 140 m4a audio only tiny 129k , m4a_dash container, mp4a.40.2@129k (44100Hz), 127.36MiB choices=" bb Best of Both $(youtube-dl "$videoId" -F | sed -n '/format code/,$ p' | sed 1d)" fi if useRofi; then code="$( echo "$choices" | rofi -dmenu -prompt "Quality " -m | cut -d ' ' -f 1 )" else code="$( echo "$choices" | fzf --prompt "Quality " -m | cut -d ' ' -f 1 )" fi code="$(echo "$code" | tr '\n' '+' | sed 's/+$//')" case "$code" in "bb") mpv "https://www.youtube.com/watch?v=$videoId" --ytdl-format="bestvideo+bestaudio" ;; *) mpv "https://www.youtube.com/watch?v=$videoId" --ytdl-format="$code" ;; esac } main(){ local url="" local videoID="" local searchTerm="" while [[ "$1" = -?* ]]; do case "$1" in -u|--url) if [ -n "$2" ]; then url="$2" shift else url="$(cat -)" fi shift ;; --rofi) rofi=yes shift ;; esac done if [ -n "$url" ]; then case "$url" in # If it contains an equals sign, let's assume it's a url # Get the value of the v parameter *=*) videoID="$(echo "$url" | grep -Eo '(\?|&)v=[^?&]*' | cut -d '=' -f 2)" ;; # If there isn't an =, assume it is just the video ID *) videoID="$url" esac chooseQuality "$videoID" else searchTerm="$*" [ -z "$searchTerm" ] && searchTerm="$(getSearchTerm)" performSearch "$searchTerm" | extractVideoInfo | chooseVideo | chooseQuality fi } main "$@"