#!/bin/sh
######################################
#> https://github.com/pystardust/ytfzf
######################################
############################
# Defaults #
############################
[ -z "$YTFZF_HIST" ] && YTFZF_HIST=1
[ -z "$YTFZF_LOOP" ] && YTFZF_LOOP=0
[ -z "$YTFZF_CUR" ] && YTFZF_CUR=1
[ -z "$YTFZF_CACHE" ] && YTFZF_CACHE="$HOME/.cache/ytfzf"
[ -z "$YTFZF_PREF" ] && YTFZF_PREF=""
[ -z "$YTFZF_EXTMENU" ] && YTFZF_EXTMENU='dmenu -i -l 30 -p Search:'
[ -z "$YTFZF_EXTMENU_LEN" ] && YTFZF_EXTMENU_LEN=220
## player settings
[ -z "$YTFZF_PLAYER" ] && YTFZF_PLAYER="mpv"
[ -z "$YTFZF_PLAYER_FORMAT" ] && YTFZF_PLAYER_FORMAT="mpv --ytdl-format="
#> Clearing/Enabling fzf_defaults
[ -z "$YTFZF_ENABLE_FZF_DEFAULT_OPTS" ] && YTFZF_ENABLE_FZF_DEFAULT_OPTS=0
[ $YTFZF_ENABLE_FZF_DEFAULT_OPTS -eq 0 ] && FZF_DEFAULT_OPTS=""
#> files and directories
history_file="$YTFZF_CACHE"/ytfzf_hst
current_file="$YTFZF_CACHE"/ytfzf_cur
thumb_dir="$YTFZF_CACHE"/thumb
#> make folders that don't exist
[ -d $YTFZF_CACHE ] || mkdir -p $YTFZF_CACHE
[ -d $thumb_dir ] || mkdir -p $thumb_dir
## For MacOS users
if [ "$(uname)" = "OpenBSD" ]; then
alias sed='\gsed'
fi
#> Setting and reading the config file
config_dir="$HOME/.config/ytfzf"
config_file="$config_dir/conf.sh"
if [ -e $config_file ]
then
#source config file if exists (overrides env variables)
. "$config_file"
fi
#> settings only set in config
search_prompt="${search_prompt-Search Youtube: }"
# DEP CHECK
dep_ck () {
for dep in "$@"; do
command -v "$dep" 1>/dev/null || { printf "$dep not found. Please install it.\n" ; exit 2; }
done
}
dep_ck "jq" "youtube-dl";
[ "$YTFZF_PLAYER" = "mpv" ] && dep_ck "mpv"
############################
# Help Texts #
############################
helpinfo () {
printf "Usage: %bytfzf [OPTIONS] %b<search-query>%b\n" "\033[1;32m" "\033[1;33m" "\033[0m";
printf " OPTIONS:\n"
printf " -h, --help Show this help text\n";
printf " -t, --thumbnails Show thumbnails (requires ueberzug)\n";
printf " Doesn't work with -H -D\n";
printf " -D, --ext-menu Use external menu(default dmenu) instead of fzf \n";
printf " -H, --choose-from-history Choose from history \n";
printf " -x, --clear-history Delete history\n";
printf " -m, --audio-only <search-query> Audio only (for music)\n";
printf " -d, --download <search-query> Download to current directory\n";
printf " -f <search-query> Show available formats before proceeding\n";
printf " -a, --auto-play <search-query> Auto play the first result, no selector\n";
printf " -r --random-play <search-query> Auto play a random result, no selector\n";
printf " -n, --video-count= <video-count> To specify number of videos to select with -a or -r\n";
printf " -l, --loop <search-query> Loop: prompt selector again after video ends\n";
printf " -s <search-query> After the video ends make another search \n";
printf " -L, --link-only <search-query> Prints the selected URL only, helpful for scripting\n";
printf " Use - instead of <search-query> for stdin\n";
printf "\n"
printf " Option usage:\n"
printf " ytfzf -fDH to show history using external \n"
printf " menu and show formats\n"
printf " ytfzf -fD --choose-from-history same as above\n"
printf "\n"
printf " Defaults can be modified through ENV variables\n";
printf " Defaults:\n";
printf " YTFZF_HIST=1 0 : off history\n";
printf " YTFZF_CACHE=~/.cache/ytfzf\n";
printf " YTFZF_LOOP=0 1 : loop the selection prompt\n";
printf " YTFZF_PREF='' 22: 720p, 18: 360p\n";
printf " YTFZF_CUR=1 For status bar bodules\n";
printf " YTFZF_EXTMENU=' dmenu -i -l 30'\n";
printf " To use rofi\n";
printf " YTFZF_EXTMENU=' rofi -dmenu -fuzzy -width 1500'\n";
printf " YTFZF_ENABLE_FZF_DEFAULT_OPTS=0 1 : fzf will use FZF_DEFAULT_OPTS\n";
printf "\n";
printf " For more details refer https://github.com/pystardust/ytfzf\n";
}
usageinfo () {
printf "Usage: %bytfzf %b<search query>%b\n" "\033[1;32m" "\033[1;33m" "\033[0m";
printf " 'ytfzf -h' for more information\n";
}
errinfo () {
printf "Check for new versions and report at: https://github.com/pystardust/ytfzf\n"
}
############################
# Formatting #
############################
#> To determine the length of each field (title, channel ... etc)
format_ext_menu () {
frac=$(((YTFZF_EXTMENU_LEN - 5 - 12)/11))
title_len=$((frac * 6 - 1))
channel_len=$((frac * 3/2))
dur_len=$((frac * 1))
view_len=$((frac * 1))
date_len=$((frac * 3/2 + 100 ))
url_len=12
}
format_fzf () {
dur_len=7
view_len=10
date_len=14
url_len=12
#> Get the terminal size to adjust length accordingly
t_size="$(stty size 2> /dev/null | cut -f2 -d' ')"
if [ "$t_size" = "" ]; then
printf "\e[31mWhen using stdin put - for the search query\033[000m\n" && exit 2
fi
if [ $t_size -lt 75 ]; then
# title channel
frac=$(((t_size - 1)/4))
title_len=$((frac * 3))
channel_len=$((frac * 1 + 7))
elif [ $t_size -lt 95 ]; then
# title channel time
frac=$(((t_size - 4)/8))
title_len=$((frac * 5 - 1))
channel_len=$((frac * 2 - 1))
dur_len=$((frac * 1 + 10))
elif [ $t_size -lt 110 ]; then
# title channel time views
frac=$(((t_size - 1)/9))
title_len=$((frac * 5 ))
channel_len=$((frac * 2 ))
dur_len=$((frac * 1))
view_len=$((frac * 1 + 7))
else
# title channel time views date
frac=$(((t_size - 5)/11))
title_len=$((frac * 5 - 1))
channel_len=$((frac * 2))
dur_len=$((frac * 1))
view_len=$((frac * 1))
date_len=$((frac * 2 + 20))
fi
}
#> Formats the fields depending on which menu is needed. And assigns the menu command.
format_menu () {
if [ $is_ext_menu -eq 0 ]; then
dep_ck "fzf"
menu_command='fzf -m --bind change:top --tabstop=1 --layout=reverse --delimiter="$(printf "\t")" --nth=1,2 $FZF_DEFAULT_OPTS'
if [ $is_stdin -eq 0 ] ; then
format_fzf
else
format_ext_menu
fi
else
# dmenu doesnt render tabs so removing it
menu_command='tr -d "$(printf "\t")" | '"$YTFZF_EXTMENU"
format_ext_menu
fi
}
#> Trucating and formatting the fields based on the decided lengths
format_awk () {
printf "%s" "$*" | awk -F'\t' \
-v A=$title_len -v B=$channel_len -v C=$dur_len -v D=$view_len -v E=$date_len -v F=$url_len \
'{ printf "%-"A"."A"s\t%-"B"."B"s\t%-"C"."C"s\t%-"D"."D"s\t%-"E"."E"s\t%-"F"."F"s\n",$1,$2,$4,$3,$5,$6}'
}
############################
# Video selection Menu #
############################
video_menu () {
format_awk "$*" | eval "$menu_command"
}
video_menu_img () {
title_len=400
format_awk "$*" | fzf -m --tabstop=1 --bind change:top --delimiter="$(printf "\t")" --nth=1,2 $FZF_DEFAULT_OPTS \
--layout=reverse --preview "sh $0 -U {}" \
--preview-window "left:50%:noborder:wrap"
}
############################
# Image previews #
############################
## The following snippet of code has been copied and modified from
# https://github.com/OliverLew/fontpreview-ueberzug MIT License
# Ueberzug related variables
FIFO="/tmp/ytfzf-ueberzug-fifo"
IMAGE="/tmp/ytfzf-ueberzug-img.png"
ID="ytfzf-ueberzug"
WIDTH=$FZF_PREVIEW_COLUMNS
HEIGHT=$FZF_PREVIEW_LINES
start_ueberzug () {
[ -e $FIFO ] || { mkfifo "$FIFO" || exit 1 ; }
[ -e $IMAGE ] || { touch "$IMAGE" || exit 1 ; }
ueberzug layer --parser json --silent < "$FIFO" &
exec 3>"$FIFO"
}
stop_ueberzug () {
exec 3>&-
rm "$FIFO" "$IMAGE" > /dev/null 2>&1
}
preview_img () {
shorturl="$(echo "$@" | sed -E -e 's_.*\|([^|]+) *$_\1_')"
# In fzf the cols and lines are those of the preview pane
IMAGE="$thumb_dir/$shorturl.png"
{ printf '{ "action": "add", "identifier": "%s", "path": "%s",' "$ID" "$IMAGE"
printf '"x": %d, "y": %d, "scaler": "fit_contain",' 2 10
printf '"width": %d, "height": %d }\n' "$WIDTH" "$((HEIGHT - 2))"
} > "$FIFO"
echo "$@" | tr -d '|' | sed "s/ *\t/\t/g" | awk -F'\t' \
'{ printf "\n%s\nChannel: %s\nViews: %s\nDuration: %s\nUploaded %s\n",$1,$2,$4,$3,$5}'
}
############################
# Scraping #
############################
download_thumbnails () {
thumb_urls="$(printf "%s" "$videos_json" |\
jq '.thumbnail.thumbnails[0].url,.videoId' )"
[ $show_link_only -eq 0 ] && printf "Downloading Thumbnails.."
rm -r $thumb_dir/* 1>/dev/null 2>&1
i=0
while read line; do
[ $((i % 2)) -eq 0 ] && { url="$(echo $line | sed -E 's/^"//;s/\.png.*/\.png/')" ;}
[ $((i % 2)) -eq 1 ] && {
name="$(echo "$line" | tr -d '"')"
{ curl -s "$url" > "$thumb_dir/$name.png" && [ $show_link_only -eq 0 ] && printf "."; } &
}
i=$((i + 1))
done << EOF
$thumb_urls
EOF
wait && [ $show_link_only -eq 0 ] && echo ""
}
scrape_yt () {
# needs search_query
## Scrape data and store video information in videos_data ( and thumbnails download)
## GETTING DATA
yt_html="$(
useragent='user-agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.152 Safari/537.36'
curl 'https://www.youtube.com/results' -s \
-G --data-urlencode "search_query=$*" \
-H 'authority: www.youtube.com' \
-H "$useragent" \
-H 'accept-language: en-US,en;q=0.9' \
--compressed
)"
if [ -z "$yt_html" ]; then
printf "\033[31mERROR[#01]: Couldn't curl website. Please check your network and try again.\033[000m\n"
errinfo
exit 2
fi
yt_json="$(printf "%s" "$yt_html" | sed -n '/var *ytInitialData/,$p' | tr -d '\n' |\
sed -E ' s_^.*var ytInitialData ?=__ ; s_;</script>.*__ ;'
)"
if [ -z "$yt_json" ]; then
printf "\033[31mERROR[#02]: Couldn't find data on site.\033[000m\n"
errinfo
exit 2
fi
videos_json="$(printf "%s" "$yt_json" | jq '..|.videoRenderer?' | sed '/^null$/d')"
videos_data="$( printf "%s" "$videos_json" |\
jq '.title.runs[0].text,.longBylineText.runs[0].text,.shortViewCountText.simpleText,.lengthText.simpleText,.publishedTimeText.simpleText,.videoId' |\
sed 's/^"//;s/"$//;s/\\"//g' | sed -E -n ";N;N;N;N;N;s/\n/\t\|/g;p"
)"
[ -z "$videos_data" ] && { printf "No results found. Try different keywords.\n"; exit 1;}
[ $show_thumbnails -eq 1 ] && download_thumbnails
}
############################
# User selection #
############################
#> To get search query
get_search_query () {
if [ $is_stdin -eq 1 ]; then
while read line; do
search_query="$search_query $line"
done
else
if [ -z "$search_query" ]; then
if [ $is_ext_menu -eq 1 ]; then
search_query="$(printf "" | $YTFZF_EXTMENU)"
else
printf "$search_prompt"
read search_query
fi
[ -z "$search_query" ] && { exit 0; }
fi
fi
}
#> To select videos from videos_data
user_selection () {
[ $is_url -eq 1 ] && return
format_menu
if [ $auto_select -eq 1 ] ; then
selected_data="$(echo "$videos_data" | sed ${link_count}q )" ;
elif [ $random_select -eq 1 ] ; then
selected_data="$(echo "$videos_data" | shuf -n $link_count )"
elif [ $show_thumbnails -eq 1 ] ; then
dep_ck "ueberzug"
start_ueberzug
selected_data="$( video_menu_img "$videos_data" )"
stop_ueberzug
else
selected_data="$( video_menu "$videos_data" )"
fi
shorturls="$(echo "$selected_data" | sed -E -e 's_.*\|([^|]+) *$_\1_')"
[ -z "$shorturls" ] && exit;
urls=""
selected_data=""
while read surl; do
[ -z "$surl" ] && continue
urls="$urls\nhttps://www.youtube.com/watch?v=$surl"
selected_data="$selected_data\n$(echo "$videos_data" | grep -m1 -e "$surl" )"
done<<EOF
$shorturls
EOF
urls="$( printf "$urls" | sed 1d )"
selected_data="$( printf "$selected_data" | sed 1d )"
if [ $show_link_only -eq 1 ] ; then
echo "$urls"
exit
fi
# select format if flag given
if [ $show_format -eq 1 ]; then
YTFZF_PREF="$(youtube-dl -F "$(printf "$urls" | sed 1q)" | sed '1,3d' | tac - |\
eval "$menu_command" | sed -E 's/^([^ ]*) .*/\1/')"
[ -z $YTFZF_PREF ] && exit;
fi
}
play_url () {
#> output the current track to current file before playing
[ $YTFZF_CUR -eq 1 ] && printf "$selected_data" > "$current_file" ;
#> Play url with $player or $player_format based on options
#> if player format fails, then use normal player
[ -n "$YTFZF_PREF" ] && {
eval "$YTFZF_PLAYER_FORMAT"\'$YTFZF_PREF\' "\"$(printf "$urls" | tr '\n' '\t' | sed 's_\t_" "_g' )\""
} || {
[ 4 -eq $? ] || eval "$YTFZF_PLAYER" "\"$(printf "$urls" | tr '\n' '\t' | sed 's_\t_" "_g' )\""
# Ctr-C in MPV results in a return code of 4
} || {
printf "\033[31mERROR[#03]: Couldn't play the video/audio using the current player: ${YTFZF_PLAYER}\n\t\033[000mTry updating youtube-dl\n"; errinfo ; save_before_exit ; exit 2;
}
}
save_before_exit () {
[ $is_url -eq 1 ] && exit
[ $YTFZF_HIST -eq 1 ] && printf "$selected_data\n" >> "$history_file" ;
[ $YTFZF_CUR -eq 1 ] && printf "" > "$current_file" ;
}
############################
# Misc #
############################
#> if the input is a url then skip video selection and play the url
check_if_url () {
# to check if given input is a url
regex='^https\?://.*'
echo "$1" | grep -q "$regex"
if [ $? -eq 0 ] ; then
is_url=1
urls="$1"
scrape=0
else
is_url=0
fi
}
#> Loads history in videos_data
get_history () {
if [ $YTFZF_HIST -eq 1 ]; then
[ -e "$history_file" ] || touch "$history_file"
hist_data="$(tac "$history_file")"
[ -z "$hist_data" ] && printf "History is empty!\n" && exit;
videos_data="$(echo "$hist_data" | uniq )"
scrape=0
else
printf "History is not enabled. Please enable it to use this option (-H).\n";
exit;
fi
}
# Opt variables
is_ext_menu=0
show_thumbnails=0
show_link_only=0
scrape=1
auto_select=0
random_select=0
search_again=0
show_format=0
link_count=1
#OPT
parse_opt () {
#the first arg is the option
opt=$1
#second arg is the optarg
OPTARG="$2"
case ${opt} in
#Long options
-)
#if the option has a short version it calls this function with the opt as the shortopt
case "${OPTARG}" in
help) parse_opt "h" ;;
ext-menu) parse_opt "D" ;;
download) parse_opt "d" ;;
choose-from-history) parse_opt "H" ;;
clear-history) parse_opt "x" ;;
random-select) parase_opt "r" ;;
search) parse_opt "s" ;;
loop) parse_opt "l" ;;
thumbnails) parse_opt "t" ;;
link-only) parse_opt "L" ;;
video-count=*) parse_opt "n" "${OPTARG#*=}" ;;
audio-only) parse_opt "m" ;;
auto-play) parse_opt "a" ;;
random-auto-play) parse_opt "r" ;;
*)
printf "Illegal option --$OPTARG\n"
usageinfo
exit 2;
;;
esac ;;
#Short options
h) helpinfo
exit ;;
D) is_ext_menu="1" ;;
m) YTFZF_PREF="bestaudio" ;;
d) YTFZF_PLAYER="youtube-dl"
YTFZF_PLAYER_FORMAT="youtube-dl -f " ;;
f) show_format=1 ;;
H) get_history ;;
x) [ -e "$history_file" ] && (echo "" > "$history_file") && printf "History has been cleared\n"
exit ;;
a) auto_select=1 ;;
r) random_select=1 ;;
s) search_again=1
YTFZF_LOOP=1 ;;
l) YTFZF_LOOP=1 ;;
t) show_thumbnails=1 ;;
L) show_link_only=1 ;;
n) link_count="$OPTARG" ;;
U) [ -p "$FIFO" ] && preview_img "$OPTARG"; exit;
# This option is reserved for the script, to show image previews
# Not to be used explicitly
;;
*)
usageinfo
exit 2 ;;
esac
}
while getopts "LhDmdfxHarltsn:U:-:" opt; do
parse_opt "$opt" "$OPTARG"
done
shift $((OPTIND-1))
[ "$*" = "-" ] && is_stdin=1 || is_stdin=0
search_query="$*"
check_if_url "$search_query"
# If in auto select mode don't download thumbnails
[ $auto_select -eq 1 ] || [ $random_select -eq 1 ] && show_thumbnails=0;
#call scrape function
if [ $scrape -eq 1 ]; then
get_search_query
scrape_yt "$search_query"
fi
while true; do
user_selection
play_url
save_before_exit
#if looping isn't on then exit
[ $YTFZF_LOOP -eq 0 ] && exit;
#if -s was specified make another search query
if [ $search_again -eq 1 ]; then
search_query=""
get_search_query
scrape_yt "$search_query"
fi
done