#!/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%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 Audio only (for music)\n"; printf " -d, --download Download to current directory\n"; printf " -f Show available formats before proceeding\n"; printf " -a, --auto-play Auto play the first result, no selector\n"; printf " -r --random-play Auto play a random result, no selector\n"; printf " -n, --video-count= To specify number of videos to select with -a or -r\n"; printf " -l, --loop Loop: prompt selector again after video ends\n"; printf " -s After the video ends make another search \n"; printf " -L, --link-only Prints the selected URL only, helpful for scripting\n"; printf " Use - instead of 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%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_;.*__ ;' )" 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< 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