Created
August 30, 2025 15:36
-
-
Save Urpagin/aff93fe938b40d0dfbc38c5bf89ad62f to your computer and use it in GitHub Desktop.
Bash script that plays a random media file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # Author: Urpagin | |
| # Date: 2025-08-30 | |
| # Description: Plays a random audio/video file given a directory. | |
| # Default directory is cwd. | |
| # Licence: MIT | |
| set -euo pipefail | |
| # Exits the program gracefully following user manual termination signal. | |
| _handle_ctrlc() { | |
| printf '\nUser termination detected; exiting...\n' | |
| exit 130 | |
| } | |
| trap '_handle_ctrlc' SIGINT | |
| # All the required commands. | |
| # If one is missing, the script will NOT continue. | |
| readonly REQ_CMDS=( file shuf echo printf getopts realpath ) | |
| _check_cmds() { | |
| for cmd in "${REQ_CMDS[@]}"; do | |
| if ! command -v "$cmd" >/dev/null 2>&1; then | |
| echo "$cmd is required but not installed" >&2 | |
| exit 1 | |
| fi | |
| done | |
| } | |
| # Exit if a required command is missing. | |
| _check_cmds | |
| _usage() { | |
| cat <<EOF | |
| Usage: | |
| $(basename "$0") [OPTIONS] ENTRY... | |
| Options: | |
| -a Select audio | |
| -v Select video | |
| -l Loops over selected files, infinitely | |
| -A Force player to be the same as audio files for all files | |
| -h Show this help message | |
| ENTRY: | |
| Files and directories. | |
| DESCRIPTION: | |
| Plays a random audio/video file. | |
| BEHAVIOUR: | |
| - Recursively traverses passed in entries. | |
| - By default, -av flags are specified and selects from the current directory. | |
| The base command is equivalent to: $(basename "$0") -av . | |
| Examples: | |
| Plays a random audio or video file from the current directory (recursive): | |
| $(basename "$0") | |
| Plays a random audio or video file and selects it from ~/Music/ and ~/Videos/: | |
| $(basename "$0") -av ~/Music/ ~/Videos/ | |
| Plays a random audio file: | |
| $(basename "$0") -a ~/Music/ | |
| Plays a random video file: | |
| $(basename "$0") -v ~/Videos/ | |
| Plays videos using the audio player (no video, only audio): | |
| $(basename "$0") -A ~/Videos/ | |
| Be sure no video pops up: | |
| $(basename "$0") -aA ~/Music/ | |
| Plays random video files in the user directory: | |
| $(basename "$0") -vl ~ | |
| EOF | |
| } | |
| sel_audio=false | |
| do_use_audio_player=false | |
| sel_video=false | |
| is_looping=false | |
| # Thanks: https://flokoe.github.io/bash-hackers-wiki/howto/getopts_tutorial/#calling-it-with-option-arguments | |
| while getopts ':aAvlh' opt; do | |
| case "$opt" in | |
| a) | |
| sel_audio=true | |
| ;; | |
| A) | |
| do_use_audio_player=true | |
| ;; | |
| v) | |
| sel_video=true | |
| ;; | |
| l) | |
| is_looping=true | |
| ;; | |
| h) | |
| _usage | |
| exit 0 | |
| ;; | |
| \?) | |
| echo "Invalid options -${OPTARG}" >&2 | |
| _usage | |
| exit 1 | |
| ;; | |
| esac | |
| done | |
| # Shift parsed arguments. | |
| shift $((OPTIND - 1)) | |
| # global variable. | |
| # We'll cache the output of find in here. | |
| cached_find='' | |
| # Thanks: https://unix.stackexchange.com/questions/217628/cut-string-on-last-delimiter | |
| # Builds the cache of found files: cached_find | |
| # Input: mass args $@ | |
| # Returns: nothing. | |
| _build_cache() { | |
| # for optimization, put this outside of this function. | |
| # But laziness got the better of me. | |
| local regex_sel | |
| regex_sel='audio|video' # default case | |
| # -av is by default, no need to add an if for it. | |
| [[ $sel_audio = true && $sel_video = false ]] && regex_sel='audio' | |
| [[ $sel_audio = false && $sel_video = true ]] && regex_sel='video' | |
| if [[ -n "$cached_find" ]]; then | |
| return | |
| fi | |
| # Use fd if it exist, for faster lookups. | |
| echo "Finding your files..." | |
| if command -v fd >/dev/null 2>&1; then | |
| cached_find="$(fd -HI --type f "$@" \ | |
| -x file --mime-type {} \ | |
| | grep -E "$regex_sel" \ | |
| | cut -d: -f1)" | |
| else | |
| cached_find="$(find "$@" \ | |
| -type f \ | |
| -exec file --mime-type {} + \ | |
| | grep -E "$regex_sel" \ | |
| | rev \ | |
| | cut -d':' -f2- \ | |
| | rev)" | |
| fi | |
| echo "Found $(printf '%s\n' "$cached_find" | wc -l) files!" | |
| } | |
| # Input: File path in $1 | |
| _play_file() { | |
| local f=${1:-} | |
| [[ -z $f ]] && { echo "No file found" >&2; exit 1; } | |
| printf "Now playing:\n%s\n" "$(realpath --relative-to="$PWD" "$f")" | |
| # If audio file. | |
| if file --mime-type "$f" | grep -q -E 'audio' || [[ "$do_use_audio_player" = true ]]; then | |
| # Audio: no window, quiet, predictable exit | |
| mpv --no-video --gapless-audio=no \ | |
| --idle=no --keep-open=no --msg-level=all=warn -- "$f" | |
| else | |
| # Video: fullscreen, HW decode, quiet, predictable exit | |
| mpv --fs --hwdec=auto-safe \ | |
| --idle=no --keep-open=no --msg-level=all=warn -- "$f" | |
| fi | |
| } | |
| # Plays a random file from mass arguments. | |
| # Input: mass args $@ | |
| _playrandom() { | |
| # Build the find cache. | |
| _build_cache "$@" | |
| # Select a random file | |
| local selected | |
| selected=$(echo "$cached_find" | shuf -n 1) | |
| if [[ -z "$selected" ]]; then | |
| echo "No matching files found." >&2 | |
| exit 1 | |
| fi | |
| # Play it | |
| _play_file "$selected" | |
| } | |
| # Input: mass args $@ | |
| _main() { | |
| # No args = cwd | |
| if [[ "$#" -eq 0 ]]; then | |
| # Unset all args and set them to '.', so $1 = . | |
| set -- '.' | |
| fi | |
| is_first=true | |
| while [[ "$is_looping" = true || "$is_first" = true ]]; do | |
| is_first=false | |
| _playrandom "$@" | |
| done | |
| } | |
| _main "$@" | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment