#!/usr/bin/env bash
-# Desc
+# Description:
# - Finds audio and video files with specified extensions up to a max depth of 8.
# - Uses ffprobe to measure their durations.
# - Creates a playlist starting at a random location within the total runtime.
# - Stores durations and file paths in a dotfile for faster subsequent runs.
# - Prompts the user whether to use the cached data or regenerate it.
+# - Uses an EDL file to specify the starting offset for the first file.
# Usage: rand_media_pl.sh [DIR]
-# Version: 0.0.1
-# Dependencies: Bash 4, ffprobe, mpv
+# Version: 0.0.4
+# Dependencies: Bash 4, ffprobe, mpv, bc, shuf
-yell() { echo "$0: $*" >&2; } # print script path and all args to stderr
-die() { yell "$*"; exit 111; } # same as yell() but non-zero exit status
-must() { "$@" || die "cannot $*"; } # runs args as command, reports args if command fails
+yell() { echo "$0: $*" >&2; } # Print script path and all args to stderr
+die() { yell "$*"; exit 111; } # Same as yell() but exits with code 111
+must() { "$@" || die "cannot $*"; } # Runs args as command, reports args if command fails
# Configurable variables
-SEARCH_DIR="$1";
-MAX_DEPTH=8;
-EXTENSIONS=("*.flac" "*.mp3" "*.opus" "*.m4a" "*.m4b" "*.mp4" "*.mkv" "*.webm");
-CACHE_FILE=".playlist_cache";
+SEARCH_DIR="${1:-.}" # Default to current directory if no argument is provided
+MAX_DEPTH=8
+EXTENSIONS=("*.flac" "*.mp3" "*.opus" "*.m4a" "*.m4b" "*.mp4" "*.mkv" "*.webm")
+CACHE_FILE=".playlist_cache"
-declare -gA file_durations;
-declare total_duration;
+declare -gA file_durations
+declare total_duration
# Function to prompt the user
prompt_yes_no() {
- yell "STATUS:User input required.";
+ yell "STATUS: User input required."
local prompt_message="$1"
local user_input
while true; do
[Yy]*) return 0 ;;
[Nn]*) return 1 ;;
*) echo "Please answer yes or no." ;;
- esac;
- done;
-};
+ esac
+ done
+}
# Function to find files with specified extensions
find_media_files() {
- yell "STATUS:Finding media files.";
- local find_cmd=("find" "-L" "$SEARCH_DIR" "-maxdepth" "$MAX_DEPTH" "-type" "f" "(");
- #declare -p find_cmd 1>&2; # debug
+ yell "STATUS: Finding media files."
+ local find_cmd=("find" "-L" "$SEARCH_DIR" "-maxdepth" "$MAX_DEPTH" "-type" "f" "(")
for ext in "${EXTENSIONS[@]}"; do
- find_cmd+=("-iname" "$ext" "-o");
- #declare -p find_cmd 1>&2; # debug
- done;
+ find_cmd+=("-iname" "$ext" "-o")
+ done
# Remove the last "-o" and add ")"
- unset 'find_cmd[-1]';
- find_cmd+=(")");
- "${find_cmd[@]}";
-};
+ unset 'find_cmd[-1]'
+ find_cmd+=(")")
+ "${find_cmd[@]}"
+}
# Function to generate the playlist cache
generate_playlist_cache() {
yell "Generating playlist cache. This may take a while..."
# Initialize or empty the cache file
- if [[ ! -f "$CACHE_FILE" ]]; then
- touch "$CACHE_FILE";
- else
- rm "$CACHE_FILE";
- fi;
-
+ : > "$CACHE_FILE" # Truncate or create the cache file
+
total_duration=0
while IFS= read -r file; do
# Get the duration using ffprobe
- duration=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$file")
- if [[ -n "$duration" ]]; then
+ duration=$(ffprobe -v error -show_entries format=duration \
+ -of default=noprint_wrappers=1:nokey=1 "$file")
+ declare -p file duration 1>&2 # Debugging statement
+
+ # Validate the duration
+ if [[ -n "$duration" && "$duration" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then
# Append the file path and duration to the cache file
- printf "%s|%s\n" "$file" "$duration" >> "$CACHE_FILE";
- total_duration=$(echo "$total_duration + $duration" | bc);
- fi;
- done < <(find_media_files | sort);
- total_duration_s="$(echo "scale=1; ${total_duration} / 1" | bc -l)";
- total_duration_h="$(echo "scale=1; ${total_duration} / 3600" | bc -l)";
-
- yell "Total duration of playlist: ${total_duration_s} seconds. (${total_duration_h} hours.)";
-};
+ printf "%s|%s\n" "$file" "$duration" >> "$CACHE_FILE"
+ total_duration=$(echo "$total_duration + $duration" | bc)
+ else
+ yell "WARNING: Invalid duration '$duration' for file '$file', skipping." 1>&2
+ fi
+ done < <(find_media_files | sort)
+
+ total_duration_s=$(printf "%.1f" "$total_duration")
+ total_duration_h=$(echo "scale=1; $total_duration / 3600" | bc -l)
+
+ yell "Total duration of playlist: ${total_duration_s} seconds (${total_duration_h} hours)."
+}
# Function to read the playlist cache
read_playlist_cache() {
- # Input: file_durations assoc. array
- # total_duration array
- yell "STATUS:Reading playlist cache.";
-
+ # Input: file_durations associative array
+ # total_duration scalar
+ yell "STATUS: Reading playlist cache."
+
total_duration=0
while IFS='|' read -r file duration; do
- file_durations["$file"]="$duration"
- total_duration=$(echo "$total_duration + $duration" | bc)
+ declare -p file duration 1>&2 # Debugging statement
+
+ # Validate the duration
+ if [[ -n "$duration" && "$duration" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then
+ file_durations["$file"]="$duration"
+ total_duration=$(echo "$total_duration + $duration" | bc)
+ else
+ yell "WARNING: Invalid duration '$duration' for file '$file', skipping." 1>&2
+ fi
done < "$CACHE_FILE"
-};
+}
# Main script
if [[ -f "$CACHE_FILE" ]]; then
if prompt_yes_no "Playlist cache detected. Do you want to use it? (Faster)"; then
- read_playlist_cache;
+ read_playlist_cache
else
- generate_playlist_cache;
- read_playlist_cache;
- fi;
+ generate_playlist_cache
+ read_playlist_cache
+ fi
else
- generate_playlist_cache;
- read_playlist_cache;
-fi;
+ generate_playlist_cache
+ read_playlist_cache
+fi
# Check if any files were found
if [[ ${#file_durations[@]} -eq 0 ]]; then
- declare -p file_durations 1>&2;
- die "FATAL:No media files found.";
+ declare -p file_durations 1>&2
+ die "FATAL: No media files found."
+fi
+
+# Ensure total_duration is not empty
+if [[ -z "$total_duration" ]]; then
+ die "FATAL: total_duration is empty."
fi
+# Convert total_duration to an integer
+total_duration_int=$(printf "%.0f" "$total_duration")
+
+# Check if total_duration_int is a valid integer
+if ! [[ "$total_duration_int" =~ ^[0-9]+$ ]]; then
+ die "FATAL: total_duration_int is not a valid integer."
+fi
+
+# Generate a random integer between 0 and total_duration_int - 1
+random_point=$(shuf -i 0-$((total_duration_int - 1)) -n1)
+yell "DEBUG: total_duration=$total_duration, total_duration_int=$total_duration_int, random_point=$random_point" 1>&2
+
# Get sorted list of files
mapfile -t sorted_files < <(printf '%s\n' "${!file_durations[@]}" | sort)
-# Generate a random starting point within the total duration
-random_point=$(awk -v max="$total_duration" 'BEGIN{srand(); print rand()*max}')
-
# Find the file and offset corresponding to the random starting point
accumulated_duration=0
-for file in "${sorted_files[@]}"; do
+accumulated_duration_int=0
+
+for idx in "${!sorted_files[@]}"; do
+ file="${sorted_files[$idx]}"
duration="${file_durations["$file"]}"
- new_accumulated_duration=$(echo "$accumulated_duration + $duration" | bc)
- if (( $(echo "$random_point < $new_accumulated_duration" | bc -l) )); then
- offset=$(echo "$random_point - $accumulated_duration" | bc)
+ declare -p idx file duration accumulated_duration 1>&2 # Debugging statement
+
+ # Validate the duration
+ if [[ -n "$duration" && "$duration" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then
+ new_accumulated_duration=$(echo "$accumulated_duration + $duration" | bc)
+ # Convert accumulated durations to integers for comparison
+ accumulated_duration_int=$(printf "%.0f" "$accumulated_duration")
+ new_accumulated_duration_int=$(printf "%.0f" "$new_accumulated_duration")
+ else
+ yell "WARNING: Invalid duration '$duration' for file '$file', skipping." 1>&2
+ continue
+ fi
+
+ if (( random_point < new_accumulated_duration_int )); then
+ offset=$(echo "$random_point - $accumulated_duration_int" | bc)
yell "Starting playback from $offset seconds into file: $file"
- # Create a playlist file
- playlist_file=$(mktemp)
- printf '%s\n' "${sorted_files[@]}" > "$playlist_file"
+ # Create an EDL file
+ edl_file=$(mktemp)
+ echo "# mpv EDL v0" > "$edl_file"
+
+ # First line: file with offset
+ printf '%s,%s\n' "$file" "$offset" >> "$edl_file"
+
+ # Append the rest of the files
+ declare -p file offset idx sorted_files edl_file 1>&2; # debug
+ for (( i=idx+1; i<${#sorted_files[@]}; i++ )); do
+ next_file="${sorted_files[$i]}"
+ yell "STATUS:Adding:$next_file";
+ printf '%s\n' "$next_file" >> "$edl_file"
+ done
# Start playback using mpv
- mpv --playlist="$playlist_file" --start="$offset" \
- --playlist-start=$(($(printf '%s\n' "${sorted_files[@]}" | grep -n "^$file$" | cut -d: -f1)-1))
+ mpv "$edl_file"
- # Securely delete the playlist file
- if [[ -n "$playlist_file" && -f "$playlist_file" ]]; then
- rm "$playlist_file"; exit 0;
+ # Securely delete the EDL file
+ if [[ -n "$edl_file" && -f "$edl_file" ]]; then
+ rm "$edl_file"
+ exit 0
else
- die "FATAL: playlist_file is either unset or not a valid file, skipping deletion.";
- fi;
+ die "FATAL: edl_file is either unset or not a valid file, skipping deletion."
+ fi
fi
- accumulated_duration="$new_accumulated_duration";
+ accumulated_duration="$new_accumulated_duration"
+ accumulated_duration_int="$new_accumulated_duration_int"
done
-die "FATAL: Could not find file corresponding to random point.";
-
+die "FATAL: Could not find file corresponding to random point."