#!/bin/bash # MTP v1.2 - Media Tail Player # Play .part files while downloading, recording or generating TTS # r = refresh (resume with more buffer) # q = quit (saves position) G='\033[1;32m' Y='\033[1;33m' C='\033[1;36m' M='\033[1;35m' R='\033[0m' WATCH_LATER_BASE="$HOME/.config/mpv/watch_later" mkdir -p "$WATCH_LATER_BASE" # === FUNCTIONS === _session_dir() { local file="$1" # Use only basename so files in different dirs share the same session # e.g. party/file.m4a.part and oh/file.m4a -> same session dir # Remove '/' completely to avoid creating unintended subdirectories # e.g. "80/20 Principle" -> "8020_Principle" not "80/20_Principle" local base base=$(basename "${file%.part}") local stem="${base%.*}" local safe safe=$(echo "$stem" | tr -d '/' | tr ' ' '_') echo "$WATCH_LATER_BASE/sessions/${safe}" } _wait_for_min_size() { local file="$1" [[ "$file" != *.part ]] && return 0 local size size=$(stat -c%s "$file" 2>/dev/null || stat -f%z "$file" 2>/dev/null) if (( size < 1048576 )); then echo -e "${Y}⏳ Waiting for minimum size: 1 MB${R}" while (( size < 1048576 )); do sleep 1 size=$(stat -c%s "$file" 2>/dev/null || stat -f%z "$file" 2>/dev/null) local size_mb size_mb=$(echo "scale=2; $size / 1048576" | bc) echo -ne "\r${M}📊 Size: ${size_mb} MB ${R}" done local size_mb size_mb=$(echo "scale=2; $size / 1048576" | bc) echo -e "\n${G}✅ Minimum size reached: ${size_mb} MB${R}" fi } _create_input_conf() { local conf="$1" cat > "$conf" <<'EOF' SPACE cycle pause 9 add volume -5 0 add volume +5 , seek -10 relative+keyframes . seek +10 relative+keyframes LEFT seek -20 relative+keyframes RIGHT seek +20 relative+keyframes UP seek +120 relative+keyframes DOWN seek -120 relative+keyframes HOME seek 0 absolute r seek 0 absolute o show-text "Buffer: ${demuxer-cache-duration}s | ${demuxer-cache-time}s cached" 5000 e quit 3 u quit 42 q quit 4 EOF } _show_file_info() { local file="$1" local size_mb size_mb=$(echo "scale=2; $(stat -c%s "$file" 2>/dev/null || stat -f%z "$file" 2>/dev/null) / 1048576" | bc) local session_dir session_dir=$(_session_dir "$file") echo -e "${C}____________________________________________________________${R}" echo -e "${C} MTP v1.2 | MEDIA TAIL PLAYER ${R}" echo -e "${C}____________________________________________________________${R}" echo -e "${Y} 9 / 0 : Volume down / up ${R}" echo -e "${Y} , / . : Seek -10 / +10 sec ${R}" echo -e "${Y} ← / → : Seek -20 / +20 sec ${R}" echo -e "${Y} ↑ / ↓ : Seek +120 / -120 sec ${R}" echo -e "${Y} SPACE : Pause / Resume ${R}" echo -e "${Y} r/HOME : Restart | u: Refresh ${R}" echo -e "${Y} q : Quit | o: OSD ${R}" echo -e "${Y} e : EOF menu ${R}" echo -e "${C}____________________________________________________________${R}" if [[ "$file" == *.part ]]; then echo -e "${Y}📥 Downloading (.part)${R}" else echo -e "${G}✅ Ready${R}" fi echo -e "${G}🎵 File:${R} $(basename "$file")" echo -e "${M}📊 Size:${R} ${size_mb} MB" local saved_pos saved_pos=$(_get_saved_pos "$session_dir") if [[ -n "$saved_pos" ]]; then local min sec min=$(echo "scale=0; $saved_pos / 60" | bc 2>/dev/null || echo "0") sec=$(printf "%.0f" "$(echo "$saved_pos % 60" | bc 2>/dev/null)" 2>/dev/null || echo "0") echo -e "${G}💾 Resuming from:${R} ${min}m ${sec}s" fi echo -e "${C}____________________________________________________________${R}" } _get_saved_pos() { local session_dir="$1" local pos_file="$session_dir/position.txt" if [[ -f "$pos_file" ]]; then cat "$pos_file" fi } _save_pos_from_session() { # Read start= from any mpv watch_later file in session_dir and save to position.txt local session_dir="$1" local pos_file="$session_dir/position.txt" local saved saved=$(find "$session_dir" -type f ! -name "position.txt" 2>/dev/null \ | xargs grep -h "^start=" 2>/dev/null | tail -1 | cut -d'=' -f2) if [[ -n "$saved" ]]; then echo "$saved" > "$pos_file" fi } _run_mpv() { local file="$1" local session_dir session_dir=$(_session_dir "$file") mkdir -p "$session_dir" local input_conf input_conf=$(mktemp) trap "rm -f '$input_conf'" RETURN _create_input_conf "$input_conf" local size_at_start size_at_start=$(stat -c%s "$file" 2>/dev/null || stat -f%z "$file" 2>/dev/null) local keep_open="no" [[ "$file" == *.part ]] && keep_open="yes" # Read saved position from our own position.txt (works across .part -> .m4a switch) local start_param="" local saved_pos saved_pos=$(_get_saved_pos "$session_dir") if [[ -n "$saved_pos" ]]; then start_param="--start=${saved_pos}" fi local _MC=$'\033[1;36m' local _MR=$'\033[0m' mpv \ --volume=70 \ --keep-open="$keep_open" \ --no-input-default-bindings \ --input-conf="$input_conf" \ --demuxer-lavf-o=fflags=+nobuffer+fastseek \ --demuxer-readahead-secs=1 \ --cache=yes \ --cache-secs=10 \ --demuxer-max-bytes=50M \ --demuxer-max-back-bytes=20M \ --stream-buffer-size=2M \ --hr-seek=yes \ --hr-seek-demuxer-offset=0 \ --save-position-on-quit \ --watch-later-directory="$session_dir" \ --title="MTP: $(basename "$file")" \ --term-status-msg='${time-pos}/${duration} | V:${volume}%' \ --term-playing-msg="${_MC}▶ \${filename}${_MR}" \ $start_param \ "$file" local exit_code=$? # After mpv exits, read its watch_later file and save position to our position.txt # This works regardless of which hash mpv used internally _save_pos_from_session "$session_dir" local size_after size_at_start_mb size_after_mb size_after=$(stat -c%s "$file" 2>/dev/null || stat -f%z "$file" 2>/dev/null || echo "$size_at_start") size_at_start_mb=$(echo "scale=2; $size_at_start / 1048576" | bc) size_after_mb=$(echo "scale=2; $size_after / 1048576" | bc) local size_diff=$(( size_after - size_at_start )) echo "" echo -e "${C}____________________________________________________________${R}" echo -e "${M}📊 Size at start:${R} ${size_at_start_mb} MB" echo -e "${M}📊 Size at end:${R} ${size_after_mb} MB" if (( size_diff > 0 )); then local size_diff_mb size_diff_mb=$(echo "scale=2; $size_diff / 1048576" | bc) echo -e "${G}📈 Downloaded during playback:${R} +${size_diff_mb} MB" fi echo -e "${C}____________________________________________________________${R}" return $exit_code } _find_base_file() { local part_file="$1" local base="${part_file%.part}" if [[ -f "$base" ]]; then echo "$base" return 0 fi local stem="${base%.*}" local found found=$(ls -t "${stem}".mp4 "${stem}".mkv "${stem}".webm \ "${stem}".m4a "${stem}".mp3 "${stem}".opus \ "${stem}".ogg "${stem}".flac "${stem}".wav 2>/dev/null | head -1) echo "$found" } # Auto-switch from .part to completed file if download finished _check_and_switch() { local file="$1" [[ "$file" != *.part ]] && echo "$file" && return 0 local base base=$(_find_base_file "$file") if [[ -n "$base" ]]; then echo -e "${G}✨ Download complete — switching to: $(basename "$base")${R}" >&2 echo "$base" else echo "$file" fi } # === FILE SELECTION === if [[ -n "$1" ]]; then CURRENT_FILE="$1" if [[ ! -f "$CURRENT_FILE" ]]; then echo -e "${Y}❌ File not found: $CURRENT_FILE${R}" exit 1 fi echo -e "${C}📂 File selected from argument${R}" else CURRENT_FILE=$(ls -t *.part 2>/dev/null | head -1) if [[ -z "$CURRENT_FILE" ]]; then CURRENT_FILE=$(ls -t *.mp4 *.mkv *.webm *.avi *.mp3 *.opus \ *.ogg *.m4a *.flac *.wav 2>/dev/null | head -1) fi if [[ -z "$CURRENT_FILE" ]]; then echo -e "${Y}❌ No media files found${R}" exit 1 fi echo -e "${C}📂 Auto-selected latest file${R}" fi # === MAIN LOOP === while true; do _wait_for_min_size "$CURRENT_FILE" _show_file_info "$CURRENT_FILE" _run_mpv "$CURRENT_FILE" EXIT_CODE=$? if [[ $EXIT_CODE -eq 42 ]]; then # u pressed - refresh echo -e "${G}🔄 Refreshing...${R}" CURRENT_FILE=$(_check_and_switch "$CURRENT_FILE") if [[ "$CURRENT_FILE" == *.part ]]; then local_size=$(stat -c%s "$CURRENT_FILE" 2>/dev/null || stat -f%z "$CURRENT_FILE" 2>/dev/null) local_mb=$(echo "scale=2; $local_size / 1048576" | bc) echo -e "${M}📊 Current .part size: ${local_mb} MB${R}" else echo -e "${G}✅ Resuming from saved position...${R}" fi sleep 1 continue elif [[ $EXIT_CODE -eq 4 ]]; then # q pressed - exit immediately break elif [[ $EXIT_CODE -eq 3 || $EXIT_CODE -eq 0 ]]; then # e pressed or natural EOF - check for completed file first CURRENT_FILE=$(_check_and_switch "$CURRENT_FILE") echo -e "${G}✅ Playback paused at end of buffer (EOF)${R}" echo -e "${C}____________________________________________________________${R}" echo -e "${Y} u / SPACE : Check for more data & Resume${R}" echo -e "${Y} r / HOME : Restart file${R}" echo -e "${Y} q / Enter : Quit${R}" echo -e "${C}____________________________________________________________${R}" read -r -s -n 1 choice # Handle escape sequences (arrow keys, HOME, etc.) if [[ "$choice" == $'\033' ]]; then read -r -s -n 2 -t 0.1 seq choice="${choice}${seq}" fi if [[ "$choice" == "u" || "$choice" == "U" || "$choice" == " " ]]; then echo -e "${G}🔄 Resuming from last saved position...${R}" sleep 1 continue elif [[ "$choice" == "r" || "$choice" == "R" || "$choice" == $'\033[H' || "$choice" == $'\033OH' ]]; then session=$(_session_dir "$CURRENT_FILE") find "$session" -type f -delete 2>/dev/null echo -e "${G}⏮ Restarting file...${R}" sleep 1 continue else break fi else break fi done