|
|
|
#!/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 <string>
|
|
|
|
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/<script[^>]*>/\n/g' -e 's/<\/script>/\n/g' | grep 'ytInitialData' | head -n 1 | grep -o '{.*}'
|
|
|
|
fi
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
extractVideoInfo(){
|
|
|
|
jq '..|.videoRenderer?' | sed '/^null$/d' |
|
|
|
|
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 useRofi; then
|
|
|
|
code="$( (
|
|
|
|
echo "bb Best of Both"
|
|
|
|
youtube-dl "$videoId" -F | sed -n '/format code/,$ p' | sed 1d;
|
|
|
|
) | rofi -dmenu -prompt "Quality " -m | cut -d ' ' -f 1 )"
|
|
|
|
else
|
|
|
|
code="$( (
|
|
|
|
echo "bb Best of Both"
|
|
|
|
youtube-dl "$videoId" -F | sed -n '/format code/,$ p' | sed 1d;
|
|
|
|
) | 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 "$@"
|