diff --git a/README.org b/README.org index e198a77..f960cef 100644 --- a/README.org +++ b/README.org @@ -1,61 +1,379 @@ * tyt +:PROPERTIES: +:header-args: :tangle tyt :shebang "#!/usr/bin/env bash" +:END: - Terminal YouTube (*tyt*) is a small bash script that lets you play YouTube videos by query from the command line. +Terminal YouTube (*tyt*) is a small bash script that lets you play YouTube videos by query from the command line. +It is created via literate programming, you can find the code below. - [[./images/screenshot.png]] +[[./images/screenshot.png]] ** Features - - Search and play videos with one command - - Interactively select a video from a list - - Download a video to the current working directory +- Search and play videos with one command +- Interactively select a video from a list +- Download a video to the current working directory - There is a =--music= flag, so you can substitute /video/ with /song/ in the above list. +There is a =--music= flag, so you can substitute /video/ with /song/ in the above list. ** Execution - This project is a [[https://nixos.wiki/wiki/Flakes][Nix flake]]. - If you have a recent [[https://nixos.org/][Nix]] version and *flakes* are enabled, you can execute the script via: +This project is a [[https://nixos.wiki/wiki/Flakes][Nix flake]]. +If you have a recent *Nix* version and *flakes* are enabled, you can execute the script via: - : nix run github:Deleh/tyt -- --help +#+begin_example sh + nix run github:Deleh/tyt -- --help +#+end_example - If not you can clone the repo, make sure the dependencies listed below are fulfilled and execute /tyt/ manually: +If you are not running the [[https://nixos.org/][Nix]] package manager, you should definitely try it out. - : ./tyt --help +Anyway, this is just a shell script so clone the repo, make sure the dependencies listed below are fulfilled and there you go. + +#+begin_example sh + ./tyt --help +#+end_example ** Dependencies - If you are running tyt as *Nix flake* you don't have to care about dependencies. - A mpv version with scripts is used by default, this enables *MPRIS support* and *skipping sponsored segments* of videos. +If you are running tyt as *Nix flake* you don't have to care about dependencies. +A mpv version with scripts is used by default, this enables *MPRIS support* while playback and *skipping sponsored seqments* of videos. - If you are not running Nix, make sure the following dependencies are installed on your system and hope that everything works: +These are the dependencies of the script: - - [[https://stedolan.github.io/jq/][jq]] - - [[https://mpv.io/][mpv]] - - [[https://ytdl-org.github.io/youtube-dl/][youtube-dl]] +- [[https://stedolan.github.io/jq/][jq]] +- [[https://mpv.io/][mpv]] +- [[https://ytdl-org.github.io/youtube-dl/][youtube-dl]] + +If you are not running Nix, make sure those are available on your system and hope that everything works. ** Usage - #+begin_example - Usage: tyt [OPTIONS] QUERIES ... [OPTIONS] +#+begin_example text + Usage: + tyt [options] "" - Play YouTube videos from the command line in a convenient way. + Options: + -a* --alternative Play an alternative video (e.g. -aaa for third alternative) + -h --help Show this help message + -i --interactive Interactive mode (overrides --alternative) + -m --music Play only the audio track + -s --save Save video (or audio if -m is provided) to the current directory - OPTIONS - -h, --help Show this help message - -a*, --alternative Play an alternative video (e.g. -aaa for third - alternative) - -d, --download Download video (or audio if -m is provided) to the current - directory - -i, --interactive Interactive mode (overrides --alternative) - -m, --music Play only the audio track + Examples: - EXAMPLES - tyt Elephants Dream # Search for 'Elephants Dream' and play - # the video - tyt -m The Beatles - Yellow Submarine # Search for 'The Beatles - Yellow - # Submarine' and play only the music - tyt -i -s Elephants Dream # Search for 'Elephants Dream' - # interactively and download the - # selected video - #+end_example + Search for "Elephants Dream" and play the video: + tyt "Elephants Dream" + + Search for "The Beatles - Yellow Submarine" and play only the music: + tyt -m "The Beatles - Yellow Submarine" + + Search for "Elephants Dream" interactively and download the selected video: + tyt -i -s "Elephants Dream" +#+end_example + +** Script +*** Dependencies + +On the start of the script, it is checked if the dependencies are fulfilled. + +#+begin_src bash + missing_dependencies=false + if ! command -v jq &> /dev/null + then + echo -ne "\e[1mjq\e[0m was not found, please install it\n" + missing_dependencies=true + fi + if ! command -v mpv &> /dev/null + then + echo -ne "\e[1mmpv\e[0m was not found, please install it\n" + missing_dependencies=true + fi + if ! command -v youtube-dl &> /dev/null + then + echo -ne "\e[1myoutube-dl\e[0m was not found, please install it\n" + missing_dependencies=true + fi + if [ "$missing_dependencies" = true ] + then + exit 1 + fi +#+end_src + +*** Usage + +This function prints the usage of the script. + +#+begin_src bash + function print_usage { + echo "Usage:" + echo " tyt [options] \"\"" + echo "" + echo "Options:" + echo " -a* --alternative Play an alternative video (e.g. -aaa for third alternative)" + echo " -h --help Show this help message" + echo " -i --interactive Interactive mode (overrides --alternative)" + echo " -m --music Play only the audio track" + echo " -s --save Save video (or audio if -m is provided) to the current directory" + echo "" + echo "Examples:" + echo "" + echo " Search for \"Elephants Dream\" and play the video:" + echo " tyt \"Elephants Dream\"" + echo "" + echo " Search for \"The Beatles - Yellow Submarine\" and play only the music:" + echo " tyt -m \"The Beatles - Yellow Submarine\"" + echo "" + echo " Search for \"Elephants Dream\" interactively and download the selected video:" + echo " tyt -i -s \"Elephants Dream\"" + } +#+end_src + +*** Arguments + +At first we parse the arguments. +We have the following flags: + +- =-a* | --alternative= :: Alternative video; You can parse any amount of alternatives (e.g. =-aaa=) +- =-h | --help :: Show a help message +- =-i | --interactive= :: Interactive mode; Shows the first 10 results and queries for a selection; If this flag is set, =-a= is ignored +- =-m | --music= :: Play only the audio track of the video +- =-s | --save= :: Save the video (or audio if =-m= is set) to the current directory + +Additionally we have exacly one mandatory quoted string as query. + + + +#+begin_src bash + alternative=0 + format="flac" + interactive=false + music=false + save=false + help=false + + for arg in "$@" + do + case $arg in + -a*) + alternative="${arg:1}" + alternative="${#alternative}" + shift + ;; + --alternative) + alternative=1 + shift + ;; + -i|--interactive) + interactive=true + shift + ;; + -m|--music) + music=true + shift + ;; + -s|--save) + save=true + shift + ;; + -h|--help) + help=true + shift + ;; + ,*) + other_arguments+=("$1") + shift + ;; + esac + done + + if [ "$help" = true ] + then + print_usage + exit 0 + fi + + if [ "${#other_arguments[@]}" != "1" ] + then + print_usage + exit 1 + fi + + query="${other_arguments[0]}" +#+end_src + +*** Greeter + +If the arguments match, print a greeter. +Another greeter is printed if the flag =-m= is set. +Make sure your terminal emulator supports Unicode to see the notes. + +#+begin_src bash + if [ "$music" = false ] + then + echo -ne "\n \e[1m\ /\e[0m\n" + echo -ne " \e[1m=======\e[0m\n" + echo -ne " \e[1m| \e[31mtyt\e[0m \e[1m|\e[0m\n" + echo -ne " \e[1m=======\e[0m\n\n" + else + echo -ne "\n \e[1m\ /\e[0m ♫\n" + echo -ne " \e[1m=======\e[0m ♫\n" + echo -ne " \e[1m| \e[31mtyt\e[0m \e[1m|\e[0m\n" + echo -ne " \e[1m=======\e[0m\n\n" + fi +#+end_src + +*** Get URL and other data + +To play a video, we need to get a valid URL. +Since there are sometimes parsing errors of the JSON response, we use an endless loop to try until we get a valid response. +The first /n/ URLs are saved if an alternative download is requested. + +#+begin_src bash + i=0 + + if [ "$interactive" = true ] + then + n=10 + else + n=$((alternative+1)) + fi + + echo -ne "Searching for: \e[34m\e[1m$query\e[0m \r" + + until results=$(youtube-dl --default-search "ytsearch" -j "ytsearch$n:$query") &> /dev/null + do + + case $i in + 0) + appendix=" " + ;; + 1) + appendix=". " + ;; + 2) + appendix=".. " + ;; + ,*) + appendix="..." + ;; + esac + + echo -ne "Searching for: \e[34m\e[1m$query\e[0m $appendix\r" + + i=$(((i + 1) % 4)) + sleep 1 + + done + + echo -ne "Searching for: \e[34m\e[1m$query\e[0m \n" + + urls=$(echo $results | jq '.webpage_url' | tr -d '"') + titles=$(echo $results | jq '.fulltitle' | tr -d '"') + uploaders=$(echo $results | jq '.uploader' | tr -d '"') + + OLDIFS=$IFS + IFS=$'\n' + urls=($urls) + titles=($titles) + uploaders=($uploaders) + IFS=$OLDIFS +#+end_src + +*** Interactive selection + +If the interactive flag is present, show the first ten results and query for a video to play. + +#+begin_src bash + if [ "$interactive" = true ] + then + echo "" + selections=(0 1 2 3 4 5 6 7 8 9 q) + for i in "${selections[@]}" + do + if [ ! "$i" = "q" ] + then + echo -ne " \e[1m$i\e[0m: ${titles[$i]} (\e[33m\e[1m${uploaders[$i]}\e[0m)\n" + fi + done + echo -ne " \e[1mq\e[0m: Quit\n" + echo -ne "\nSelection: " + read selection + while [[ ! "${selections[@]}" =~ "${selection}" ]] + do + echo -ne "Not valid, try again: " + read selection + done + if [ "$selection" = "q" ] + then + exit + fi + echo "" + url=${urls[$selection]} + title=${titles[$selection]} + uploader=${uploaders[$selection]} + else + url=${urls[$alternative]} + title=${titles[$alternative]} + uploader=${uploaders[$alternative]} + fi +#+end_src + +*** Play or save video + +Finally the video is played via mpv or saved via youtube-dl. +If the =-m= flag is set, only the audio track is played or saved. + +In interaction mode, another video is queried to be played. + +#+begin_src bash + function play { + echo -ne "Playing: \e[32m\e[1m$2\e[0m (\e[33m\e[1m$3\e[0m)\n" + if [ "$music" = true ] + then + mpv --no-video "$1" &> /dev/null + else + mpv "$1" &> /dev/null + fi + } + + function download { + echo -ne "Downloading: \e[32m\e[1m$2\e[0m (\e[33m\e[1m$3\e[0m)\n" + if [ "$music" = true ] + then + youtube-dl -x -o "%(title)s.%(ext)s" "$1" &> /dev/null + else + youtube-dl -o "%(title)s.%(ext)s" "$1" &> /dev/null + fi + } + + if [ "$save" = true ] + then + download "$url" "$title" "$uploader" + else + play "$url" "$title" "$uploader" + + if [ "$interactive" = true ] + then + while : + do + echo -ne "\nSelect another or enter [q] to quit: " + read selection + while [[ ! "${selections[@]}" =~ "${selection}" ]] + do + echo -ne "Not valid, try again: " + read selection + done + if [ ! "$selection" = "q" ] + then + echo "" + url=${urls[$selection]} + title=${titles[$selection]} + uploader=${uploaders[$selection]} + play "$url" "$title" "$uploader" + else + exit + fi + done + fi + fi +#+end_src diff --git a/flake.lock b/flake.lock index 969133d..7fd9e27 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "flake-utils": { "locked": { - "lastModified": 1631561581, - "narHash": "sha256-3VQMV5zvxaVLvqqUrNz3iJelLw30mIVSfZmAaauM3dA=", + "lastModified": 1618217525, + "narHash": "sha256-WGrhVczjXTiswQaoxQ+0PTfbLNeOQM6M36zvLn78AYg=", "owner": "numtide", "repo": "flake-utils", - "rev": "7e5bf3925f6fbdfaf50a2a7ca0be2879c4261d19", + "rev": "c6169a2772643c4a93a0b5ac1c61e296cba68544", "type": "github" }, "original": { @@ -17,11 +17,12 @@ }, "nixpkgs": { "locked": { - "lastModified": 1633267966, - "narHash": "sha256-gFKvZ5AmV/dDTKXVxacPbXe4R0BsFpwtVaQxuIm2nnk=", - "path": "/nix/store/k13ripsl4n2p6wf2ksy5m017ryykx4qc-source", - "rev": "7daf35532d2d8bf5e6f7f962e6cd13a66d01a71d", - "type": "path" + "lastModified": 1618640098, + "narHash": "sha256-RPdJQX2/VcLMb04TNZtyCgHyTOjwcaM3UjBziNwGz1g=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "a03f318104db1a74791746595829de4c2d53e658", + "type": "github" }, "original": { "id": "nixpkgs", diff --git a/flake.nix b/flake.nix index 481a6cb..6d0dccf 100644 --- a/flake.nix +++ b/flake.nix @@ -1,8 +1,6 @@ { description = "Play YouTube videos from the command line in a convenient way"; - nixConfig.bash-prompt = "\[\\e[1m\\e[31mtyt-develop\\e[0m:\\w\]$ "; - inputs.flake-utils.url = "github:numtide/flake-utils"; outputs = { self, nixpkgs, flake-utils }: @@ -20,6 +18,12 @@ ]; }); + dependencies = with pkgs; [ + jq + mpv + youtube-dl + ]; + in { @@ -32,11 +36,13 @@ name = "tyt"; src = self; + buildInputs = dependencies; + patchPhase = with pkgs; '' substituteInPlace tyt \ - --replace jq ${pkgs.jq}/bin/jq \ + --replace jq ${jq}/bin/jq \ --replace mpv ${mpv}/bin/mpv \ - --replace youtube-dl ${pkgs.youtube-dl}/bin/youtube-dl + --replace youtube-dl ${youtube-dl}/bin/youtube-dl ''; installPhase = '' @@ -49,11 +55,7 @@ # Development shell devShell = pkgs.mkShell { - buildInputs = [ - pkgs.jq - mpv - pkgs.youtube-dl - ]; + buildInputs = dependencies; }; } diff --git a/images/screenshot.png b/images/screenshot.png index b4351d3..9793fc0 100644 Binary files a/images/screenshot.png and b/images/screenshot.png differ diff --git a/tyt b/tyt index 7979e46..fcc25b3 100755 --- a/tyt +++ b/tyt @@ -1,165 +1,159 @@ #!/usr/bin/env bash - -# Text formatting variables -text_bold="\e[1m" -text_red="\e[31m" -text_reset="\e[0m" -text_yellow="\e[33m" - -function print_usage { - cat <&2 - exit 1 -} - -function play { - print_controls - if [ "$music" == true ] - then - mpv --no-video --msg-level=all=no,statusline=status --term-status-msg="\${time-pos}/\${duration} - ${text_bold}${2//\\/\\\\}${text_reset} (${text_yellow}${text_bold}${3//\\/\\\\}${text_reset})" "$1" - else - mpv --msg-level=all=no,statusline=status --term-status-msg="\${time-pos}/\${duration} - ${text_bold}${2//\\/\\\\}${text_reset} (${text_yellow}${text_bold}${3//\\/\\\\}${text_reset})" "$1" - fi -} - -function download { - echo -ne "Downloading: ${text_bold}$2${text_reset} (${text_yellow}${text_bold}$3${text_reset})\n" - if [ "$music" == true ] - then - youtube-dl -x -o "%(title)s.%(ext)s" "$1" &> /dev/null - else - youtube-dl -o "%(title)s.%(ext)s" "$1" &> /dev/null - fi -} - -# Set default values -alternative=1 -download=false -interactive=false -music=false -query="" - -# Parse arguments -while (( "$#" )); do - case "$1" in - -a*) - alternative="${#1}" - shift - ;; - --alternative) - alternative=2 - shift - ;; - -d|--download) - download=true - shift - ;; - -h|--help) - print_usage - ;; - -i|--interactive) - interactive=true - shift - ;; - -m|--music) - music=true - shift - ;; - -) - query="$query $1" - shift - ;; - -*|--*=) - error "Unsupported flag: $1" - ;; - *) - query="$query $1" - shift - ;; - esac -done - -# Check dependencies +missing_dependencies=false if ! command -v jq &> /dev/null then - error "jq was not found, please install it" -elif ! command -v mpv &> /dev/null + echo -ne "\e[1mjq\e[0m was not found, please install it\n" + missing_dependencies=true +fi +if ! command -v mpv &> /dev/null then - error -ne "mpv was not found, please install it" -elif ! command -v youtube-dl &> /dev/null + echo -ne "\e[1mmpv\e[0m was not found, please install it\n" + missing_dependencies=true +fi +if ! command -v youtube-dl &> /dev/null then - error "youtube-dl was not found, please install it" + echo -ne "\e[1myoutube-dl\e[0m was not found, please install it\n" + missing_dependencies=true +fi +if [ "$missing_dependencies" = true ] +then + exit 1 fi -# Handle empty query -if [ "$query" == "" ] +function print_usage { + echo "Usage:" + echo " tyt [options] \"\"" + echo "" + echo "Options:" + echo " -a* --alternative Play an alternative video (e.g. -aaa for third alternative)" + echo " -h --help Show this help message" + echo " -i --interactive Interactive mode (overrides --alternative)" + echo " -m --music Play only the audio track" + echo " -s --save Save video (or audio if -m is provided) to the current directory" + echo "" + echo "Examples:" + echo "" + echo " Search for \"Elephants Dream\" and play the video:" + echo " tyt \"Elephants Dream\"" + echo "" + echo " Search for \"The Beatles - Yellow Submarine\" and play only the music:" + echo " tyt -m \"The Beatles - Yellow Submarine\"" + echo "" + echo " Search for \"Elephants Dream\" interactively and download the selected video:" + echo " tyt -i -s \"Elephants Dream\"" +} + +alternative=0 +format="flac" +interactive=false +music=false +save=false +help=false + +for arg in "$@" +do + case $arg in + -a*) + alternative="${arg:1}" + alternative="${#alternative}" + shift + ;; + --alternative) + alternative=1 + shift + ;; + -i|--interactive) + interactive=true + shift + ;; + -m|--music) + music=true + shift + ;; + -s|--save) + save=true + shift + ;; + -h|--help) + help=true + shift + ;; + *) + other_arguments+=("$1") + shift + ;; + esac +done + +if [ "$help" = true ] then - print_usage + print_usage + exit 0 fi -print_logo -echo -ne "Searching for: $text_bold${query:1}$text_reset\n" +if [ "${#other_arguments[@]}" != "1" ] +then + print_usage + exit 1 +fi -# Set number of videos -if [ "$interactive" == true ] +query="${other_arguments[0]}" + +if [ "$music" = false ] +then + echo -ne "\n \e[1m\ /\e[0m\n" + echo -ne " \e[1m=======\e[0m\n" + echo -ne " \e[1m| \e[31mtyt\e[0m \e[1m|\e[0m\n" + echo -ne " \e[1m=======\e[0m\n\n" +else + echo -ne "\n \e[1m\ /\e[0m ♫\n" + echo -ne " \e[1m=======\e[0m ♫\n" + echo -ne " \e[1m| \e[31mtyt\e[0m \e[1m|\e[0m\n" + echo -ne " \e[1m=======\e[0m\n\n" +fi + +i=0 + +if [ "$interactive" = true ] then n=10 else - n="$alternative" + n=$((alternative+1)) fi -# Get results -results=$(youtube-dl --default-search "ytsearch" -j "ytsearch$n:${query:1}") +echo -ne "Searching for: \e[34m\e[1m$query\e[0m \r" + +until results=$(youtube-dl --default-search "ytsearch" -j "ytsearch$n:$query") &> /dev/null +do + + case $i in + 0) + appendix=" " + ;; + 1) + appendix=". " + ;; + 2) + appendix=".. " + ;; + *) + appendix="..." + ;; + esac + + echo -ne "Searching for: \e[34m\e[1m$query\e[0m $appendix\r" + + i=$(((i + 1) % 4)) + sleep 1 + +done + +echo -ne "Searching for: \e[34m\e[1m$query\e[0m \n" + urls=$(echo $results | jq '.webpage_url' | tr -d '"') titles=$(echo $results | jq '.fulltitle' | tr -d '"') uploaders=$(echo $results | jq '.uploader' | tr -d '"') -# Create arrays OLDIFS=$IFS IFS=$'\n' urls=($urls) @@ -167,83 +161,86 @@ titles=($titles) uploaders=($uploaders) IFS=$OLDIFS -if [ "${#urls[@]}" == 0 ] -then - error "No results, try again" -fi - -# Select video if [ "$interactive" = true ] then echo "" selections=(0 1 2 3 4 5 6 7 8 9 q) for i in "${selections[@]}" do - if [ "$i" != "q" ] + if [ ! "$i" = "q" ] then - echo -e " ${text_bold}$i${text_reset}: ${titles[$i]} (${text_yellow}${text_bold}${uploaders[$i]}${text_reset})" + echo -ne " \e[1m$i\e[0m: ${titles[$i]} (\e[33m\e[1m${uploaders[$i]}\e[0m)\n" fi done - echo -e " ${text_bold}q${text_reset}: Quit\n" - echo -ne "Selection: " - read -n1 selection - echo + echo -ne " \e[1mq\e[0m: Quit\n" + echo -ne "\nSelection: " + read selection while [[ ! "${selections[@]}" =~ "${selection}" ]] do echo -ne "Not valid, try again: " - read -n1 selection - echo + read selection done - if [ "$selection" == "q" ] + if [ "$selection" = "q" ] then - echo exit fi - echo + echo "" url=${urls[$selection]} title=${titles[$selection]} uploader=${uploaders[$selection]} else - url=${urls[$((alternative-1))]} - title=${titles[$((alternative-1))]} - uploader=${uploaders[$((alternative-1))]} + url=${urls[$alternative]} + title=${titles[$alternative]} + uploader=${uploaders[$alternative]} fi -# Download or play video -if [ "$download" = true ] +function play { + echo -ne "Playing: \e[32m\e[1m$2\e[0m (\e[33m\e[1m$3\e[0m)\n" + if [ "$music" = true ] + then + mpv --no-video "$1" &> /dev/null + else + mpv "$1" &> /dev/null + fi +} + +function download { + echo -ne "Downloading: \e[32m\e[1m$2\e[0m (\e[33m\e[1m$3\e[0m)\n" + if [ "$music" = true ] + then + youtube-dl -x -o "%(title)s.%(ext)s" "$1" &> /dev/null + else + youtube-dl -o "%(title)s.%(ext)s" "$1" &> /dev/null + fi +} + +if [ "$save" = true ] then download "$url" "$title" "$uploader" else play "$url" "$title" "$uploader" -fi -if [ "$interactive" == true ] -then - while : - do - echo -ne "\nSelect another or enter [${text_bold}q${text_reset}] to quit: " - read -n1 selection - echo - while [[ ! "${selections[@]}" =~ "${selection}" ]] + + if [ "$interactive" = true ] + then + while : do - echo -ne "Not valid, try again: " - read -n1 selection - echo - done - if [ "$selection" != "q" ] - then - echo - url=${urls[$selection]} - title=${titles[$selection]} - uploader=${uploaders[$selection]} - if [ "$download" == true ] + echo -ne "\nSelect another or enter [q] to quit: " + read selection + while [[ ! "${selections[@]}" =~ "${selection}" ]] + do + echo -ne "Not valid, try again: " + read selection + done + if [ ! "$selection" = "q" ] then - download "$url" "$title" "$uploader" - else + echo "" + url=${urls[$selection]} + title=${titles[$selection]} + uploader=${uploaders[$selection]} play "$url" "$title" "$uploader" + else + exit fi - else - echo - exit - fi - done + done + fi fi