#!/bin/sh
#
# fyr - 2019 (c) MIT | /bin/sh mpvc
# control mpv remotely using JSON ipc
# https://mpv.io/manual/master/#json-ipc

SOCKET=${MPVC_SOCKET:-/tmp/mpvsocket}
MPVOPTIONS="--no-audio-display"

usage() {
    cat >&2 << EOF
Usage: $(basename $0) [-S "socket"] [-a "filenames"] [-o "path"] [-f "format string"]
    -p | --toggle       : Toggle playback.
    -s | --stop         : Always stop playback.
    -P | --play         : Always start playback.
    -f | --format       : Enter a formatting string.
    -a | --add          : Add files to playlist.
    -i | --playlist     : Print filenames of tracks to fit within terminal.
    -I | --fullplaylist : Print all filenames of tracks in current playlist.
    -o | --save         : Save current playlist to given path.
    -j | --track        : Go forwards/backwards through the playlist queue.
    -J | --tracknum     : Jump to playlist item number.
    -z | --shuffle      : Shuffle the current playlist.
    -l | --loop         : Loop currently playing playlist.
    -L | --loopfile     : Loop currently playing file.
    -v | --vol          : Increase/decrease volume relative to current volume.
    -V | --volume       : Set absolute volume.
    -m | --mute         : Toggle sound.
    -t | --seek         : Increases/decreases time relatively, accepts % values.
    -T | --time         : Set absolute time.
    -x | --speed        : Increase/decrease speed relative to the current speed.
    -X | --speedval     : Set absolute speed.
    -I | --image        : Enable adding of images to the queue.
    -k | --kill         : Kill the mpv process controlling the given socket.
    -K | --killall      : Kill all mpv processes indiscriminately.
    -S | --socket       : Set socket location [default: $SOCKET].
    -q | --quiet        : Surpress all text output.
    -Q | --vid=no       : Start mpv with video output disabled.
    -- |                : After adding files options after -- are passed to mpv.
    -h | --help         : Print this help.

Formatting:
    \`$(basename $0) --format\` will interpret the following delimiters if they are found:

    %name%, %path%, %dir%, %title%, %artist%, %album%, %albumartist%, comment%,
    %genre%, %year%, %percentage%, %playlistlength%, %position%, %repeat%,
    %single, %status%, %time%, %precisetime%, %speed%, %length%, %remaining%
    %volume%, %muted%, %frame%, %width%, %height%, %ab-loop-a%, %ab-loop-b%

MPC compatibility layer:
    mpvc features a nearly complete compatibility layer for mpc commands in
    addition to GNU style arguments. http://linux.die.net/man/1/mpc

Exit codes:
    0: Program ran succesfully.
    1: Input Argument error.
    2: Socket does not exist.
    3: Socket is not currently open.
    4: Dependency error.
EOF

    test -z "$1" && exit || exit "$1"
}

    # Retrieval Functions
###############################################################################

# match given filename string to appropriate output
# accepted properties: filename
cleanFilename() {
    filename="$1"

    case "$filename" in
        *.googlevideo.com/*)
            filename="mps-yt stream"
            printf '%s\n' "$filename"
            ;;
        *youtu*|watch*)
            # no other option but to use jq
            type jq > /dev/null 2>&1 && {
                # this is fucking quick
                id="$(printf '%s\n' "$filename" | cut -d'=' -f 2)"
                title="$(curl -sL \
"https://youtube.com/oembed?url=https://youtube.com/watch?v=$id" | \
jq -r '.title')"
            } || {
                title="$filename"
            }

            printf '%s\n' "$title"
            ;;
        *)
            # return filename to relative path
            # this will probably look funky if there's a '/' in the filename
            filename=$(basename "$filename")
            printf '%s\n' "$filename"
            ;;
    esac
}

# accepted properties: media-title, path
getMediaProperties() {
    property="$1"

    mediaValue=$(printf '%s\n' "{ \"command\": [\"get_property\", \
\"${property}\" ] }" | $SOCKETCOMMAND 2> /dev/null | awk -F '"' '{print $4}')

    printf '%s\n' "$mediaValue" | escapeSedChar '&' | escapeSedChar '#'
}

# accepted properties: date, genre, title, album, artist, album_artist
getMetadata() {
    property=$1

    metadata=$(printf '%s\n' "{ \"command\": [\"get_property\", \
\"metadata/by-key/${property}\" ] }" | $SOCKETCOMMAND 2> /dev/null | \
        tr -d "{}" | awk -F '"' '{print $4}')

    # test for no result
    test "$metadata" = "property not found" && {
        result="N/A"
    } || {
        result="$metadata"
    }

    # test if title has no property and return filename instead
    test "$property" = "title" && {
        test "$metadata" = "property not found" && {
            result=$(getPropertyString filename)
        }
        test "$metadata" = "error" && {
            result=$(getPropertyString filename)
        }
    }

    test "$property" = "title" && {
        title=$(cleanFilename "$result")
        printf '%s' "$title" | escapeSedChar '&' | escapeSedChar '#'
        return 0
    }

    printf '%s' "$result" | escapeSedChar '&' | escapeSedChar '#'
}

# retrieve integer/boolean property
# accepted properties: mute, pause, loop-file, estimated-frame-number, width, height
getProperty() {
    property=$1

    result=$(printf '%s\n' "{ \"command\": [\"get_property\", \
\"${property}\"] }" | $SOCKETCOMMAND 2> /dev/null | awk -F '[":,]' '{print $4}')

    printf '%s' "$result"
}

# accepted properties: idle-active, playlist-count, playlist-pos, playback-time,
# playtime-remaining, time-remaining, percent-pos, duration volume
getPropertyString() {
    property=$1

    result=$(printf '%s\n' "{ \"command\": [\"get_property_string\", \
\"${property}\"] }" | $SOCKETCOMMAND 2> /dev/null | awk -F '[".]' '{print $4}')

    test "$result" = "error" && {
        test $# -ge 2 && printf '%s' "$2" || printf '%s' "N/A"
        return 1
    } || {
        printf '%s' "$result"
        return 0
    }
}

# accepted properties: speed
getSpeed() {
    speed=$(printf '%s\n' "{ \"command\": [\"get_property_string\", \
\"speed\"] }" | $SOCKETCOMMAND 2> /dev/null | awk -F '"' '{print substr ($4, 0, 4)}')

    printf '%s' "$speed"
}

# accepted properties: loop-file, loop-playlist
getLoopStatus() {
    property=$1

    case "$property" in
        loop-file)
            status=$(getProperty loop-file)
            ;;
        loop-playlist)
            status=$(printf '%s\n' "{ \"command\": [\"get_property\", \
\"loop-playlist\"] }" | $SOCKETCOMMAND 2> /dev/null)
            printf '%s\n' "$status" | grep "inf" > /dev/null 2>&1 && {
                status="$(printf '%s\n' "$status" | cut -d\" -f 4)"
            } || {
                status="$(printf '%s\n' "$status" | awk -F '[":,]' '{print $4}')"
            }
            ;;
    esac

    test "$status" -ne 0 2> /dev/null
    test $? -ne 2 && {
        loop="$status"
    } || {
        case "$status" in
            true | inf)      loop="yes"     ;;
            false)           loop="no"      ;;
            *)               loop="N/A"     ;;
        esac
    }

    printf '%s' "$loop"
}

getMuteStatus() {
    muted="$(getProperty mute)"

    case "$muted" in
        true)  muted="yes" ;;
        false) muted="no"  ;;
        *)     muted="N/A" ;;
    esac

    printf '%s' "$muted"
}

getPauseStatus() {
    status="$(getProperty pause)"

    case "$status" in
        true)  status="paused"  ;;
        false) status="playing" ;;
        *)     status="N/A"     ;;
    esac

    printf '%s' "$status"
}

# accepted properties: $1: filename $2: full path flag to skip basename
getPlaylistFilename() {
    track="$1"
    fullpathFlag="$2"

    filename=$(printf '%s\n' "{ \"command\": [\"get_property_string\", \
\"playlist/$track/filename\"] }" | $SOCKETCOMMAND 2> /dev/null | \
        cut -d\" -f 4)

    test "$fullpathFlag" != "fullpath" && filename=$(cleanFilename "$filename")

    printf '%s\n' "$filename"
}

# print all filenames in current playlist
getPlaylist() {
    noColour="$1"

    tracks=$(getPropertyString playlist-count)
    currentTrack=$(getPropertyString playlist-pos)

    test "$tracks" -eq 0 -o "$currentTrack" = "N/A" && {
        printf '%s\n' "MPV instance on ${SOCKET} is currently idle." >&2
        exit 1
    }

    calculateTerminalHeight

    test "${SEQCOMMAND}" = "jot" && REPS=$((LAST - FIRST + 1))

    for i in $("$SEQCOMMAND" $REPS "$FIRST" "$LAST"); do
        filename=$(getPlaylistFilename $((i - 1)))

        test "$noColour" != "nocolour" && {
            test "$currentTrack" -eq $((i - 1)) && {
                printf '%d	[7m %s[0m\n' "$i" "${filename} "
            } || {
                printf '%d	 %s\n' "$i" "${filename}"
            }
        } || {
            printf '%d	%s\n' "$i" "${filename}"
        }
    done
}

getFullPlaylist() {
    noColour="$1"

    tracks=$(getPropertyString playlist-count)
    currentTrack=$(getPropertyString playlist-pos)

    test "$tracks" -eq 0 -o "$currentTrack" = "N/A" && {
        printf '%s\n' "MPV instance on ${SOCKET} is currently idle." >&2
        exit 1
    }

    FIRST=1
    LAST=$(getPropertyString playlist-count)

    test "${SEQCOMMAND}" = "jot" && REPS=$((LAST - FIRST + 1))

    for i in $("$SEQCOMMAND" $REPS "$FIRST" "$LAST"); do
        filename=$(getPlaylistFilename $((i - 1)))

        test "$noColour" != "nocolour" && {
            test "$currentTrack" -eq $((i - 1)) && {
                printf '%d	[7m %s[0m\n' "$i" "${filename} "
            } || {
                printf '%d	 %s\n' "$i" "${filename}"
            }
        } || {
            printf '%d	%s\n' "$i" "${filename}"
        }
    done
}

# open mpvc to retrieve all metadata from a playlist file
getFilenameMetadata() {
    intCheck "$1" || exit 1

    trackToMetadata="$1"

    filename=$(getPlaylistFilename $trackToMetadata fullpath)
    echo $filename

    # mpvc -S /tmp/tempsock -m -a "$filename" -f \
# "Artist: %artist%"
}

# saves playlist to file but with no path checking. we live dangerously
savePlaylist() {
    saveLocation="$1"

    test -e "$saveLocation.m3u" && {
        printf '%s\n' "Playlist exists! Overwrite? (y for yes, anything else no)"

        oldstty=$(stty -g)
        stty raw -echo; key="$(head -c 1)"; stty $oldstty
        case $key in
            y) rm $saveLocation.m3u ;;
            *) return               ;;
        esac
    }

    printf '%s\n' "Adding files to $saveLocation.m3u..."

    FIRST=1
    LAST=$(getPropertyString playlist-count)

    test "${SEQCOMMAND}" = "jot" && REPS=$((LAST - FIRST + 1))

    for i in $("$SEQCOMMAND" $REPS "$FIRST" "$LAST"); do
        printf '%s\n' "$(getPlaylistFilename $((i - 1)) fullpath)" >> "$saveLocation".m3u
    done

    QUIETFLAG=true
}

    # Control Functions
###############################################################################

appendTrack() {
    filename="$@"

    # require absolute paths
    test -e "$filename" -a "$(printf "%s" "$filename" | cut -c 1)" != '/' && {
        filename="$(pwd)/$filename"
    }

    # don't add images or bad files to the queue by default
    # this doesn't stop mpv from resolving directories and adding them anyway
    test "$IMAGEFLAG" != "true" && {
        case $filename in
            *.png|*.jpg|*.jpeg|*.gif|*.psd|*.pdf)
                return
                ;;
        esac
    }

    pgrep -f "mpv .*$SOCKET" > /dev/null 2>&1  && {
        printf '%s\n' "{ \"command\": [\"loadfile\", \"$filename\", \
\"append-play\" ] }" | $SOCKETCOMMAND > /dev/null 2>&1
    } || {
        exec mpv --really-quiet --idle=once --input-ipc-server="${SOCKET}" \
            $MPVOPTIONS "$filename" &

        # wait up to 5 seconds for mpv to start on $SOCKET
        for i in $($SEQCOMMAND 50); do
            idlestatus=$(getPropertyString idle-active)
            test "$idlestatus" && break
            sleep 0.01
        done
    }

    filename=$(cleanFilename "$filename")

    printf '%s\n' "Adding: ${filename}"
}

setTimeRelative() {
    time=$(getPropertyString playback-time)

    printf '%s\n' "$1" | grep "%" > /dev/null 2>&1 && {
        percentageValue=$(printf '%s\n' "$1" | rev | cut -c 2- | rev)
        printf '%s\n' "{ \"command\": [\"set_property\", \"percent-pos\", \
$percentageValue ] }" | $SOCKETCOMMAND > /dev/null
        return
    }

    sign=$(printf '%s' "$1" | cut -c 1)
    case "$sign" in
        -)
            minusFlag=true
            timeArg=$(printf '%s' "$1" | cut -c 2-)
            ;;
        +)
            timeArg=$(printf '%s' "$1" | cut -c 2-)
            ;;
        *)
            timeArg=$1
            ;;
    esac

    timeSec=$(parseTimeString "$timeArg") || exit $?

    test "$minusFlag" = "true" && {
        time=$((time - timeSec))
    } || {
        time=$((time + timeSec))
    }

    printf '%s\n' "{ \"command\": [\"set_property\", \"playback-time\", \
$time ] }" | $SOCKETCOMMAND > /dev/null
}

setTimeAbsolute() {
    time=$(parseTimeString "$1") || exit $?

    trackTime=$(getPropertyString duration)

    test "$time" -ge "$trackTime" && {
        printf '%s\n' "Given time is greater than track length! ($(trackLength))"
        QUIETFLAG=true
        return 1
    }

    printf '%s\n' "{ \"command\": [\"set_property\", \"playback-time\", \
$time ] }" | $SOCKETCOMMAND > /dev/null
}

setVolumeRelative() {
    intCheck "$1" || exit 1

    volume=$(getPropertyString volume)

    test "$volume" = "error" && {
        printf '%s\n' "Currently playing media does not have sound." >&2
        exit 1
    }

    volume=$((volume + $1))

    test $volume -lt 0 && {
        printf '%s\n' "Volume cannot be set lower than 0%" >&2
        exit 1
    }

    test $volume -gt 130 && {
        printf '%s\n' "Volume cannot be set great than 130%" >&2
        exit 1
    }

    printf '%s\n' "{ \"command\": [\"set_property\", \"volume\", $volume ] }" | \
        $SOCKETCOMMAND > /dev/null
}

setVolumeAbsolute() {
    # test if media has sound
    volume=$(getPropertyString volume)

    test "$volume" = "error" && {
        printf '%s\n' "Currently playing media does not have sound." >&2
        return 1
    }

    intCheck "$1" || exit 1
    volume=$1

    test "$volume" -lt 0 && {
        printf '%s\n' "Volume cannot be set lower than 0%" >&2
        return 1
    }

    test "$volume" -gt 130 && {
        printf '%s\n' "Volume cannot be set great than 130%" >&2
        return 1
    }

    printf '%s\n' "{ \"command\": [\"set_property\", \"volume\", $volume ] }" | \
        $SOCKETCOMMAND > /dev/null
}

setSpeedRelative() {
    validateBC
    speed=$(getSpeed)

    fltCheck "$1" || exit 1
    speed=$(printf '%s\n' "$speed+$1" | bc)

    printf '%s\n' "{ \"command\": [\"set_property_string\", \"speed\", \
\"$speed\" ] }" | $SOCKETCOMMAND > /dev/null
}

setSpeedAbsolute() {
    validateBC
    fltCheck "$1" || exit 1
    speed=$1

    printf '%s\n' "{ \"command\": [\"set_property_string\", \"speed\", \
\"$speed\" ] }" | $SOCKETCOMMAND > /dev/null
}

setTrackRelative() {
    intCheck "$1" || exit 1

    currentTrack=$(getPropertyString playlist-pos)
    desiredTrack=$((currentTrack + $1))
    trackCount=$(getPropertyString playlist-count)

    # if time is greater than 10 seconds, set time to 0
    test "$desiredTrack" -lt "$currentTrack" && {
        seconds=$(getPropertyString playback-time)
        test "$seconds" -ge 10 && {
            setTimeAbsolute 0
            return
        }
    }

    test "$desiredTrack" -ge "$trackCount" && {
        repeat=$(getLoopStatus loop-playlist)
        test "$repeat" = "yes" && {
            desiredTrack=0
        }
    }

    test "$desiredTrack" -lt 0 && {
        repeat=$(getLoopStatus loop-file)
        test "$repeat" = "yes" && {
            setTrackAbsolute "$trackCount"
            return
        } || {
            desiredTrack=0
        }
    }

    printf '%s\n' "{ \"command\": [\"set_property\", \"playlist-pos\", \
$desiredTrack ] }" | $SOCKETCOMMAND > /dev/null

    # tiny delay so printFinalOutput can catch up
    sleep 0.5
}

setTrackAbsolute() {
    intCheck "$1" || exit 1

    currentTrack=$1
    currentTrack=$((currentTrack - 1))
    trackCount=$(getPropertyString playlist-count)

    test "$currentTrack" -lt 0 || test "$currentTrack" -ge "$trackCount" && {
        printf '%s\n' "Item $currentTrack is out of range of playlist." >&2
        exit 1
    }

    printf '%s\n' "{ \"command\": [\"set_property\", \
\"playlist-pos\", $currentTrack ] }" | $SOCKETCOMMAND > /dev/null

    # tiny delay so printFinalOutput can catch up
    sleep 0.5
}

moveTrack() {
    intCheck "$1" || exit 1

    test -z "$2" && {
        trackToMove=$(getPropertyString playlist-pos)
        newTrackPosition=$1
    } || {
        trackToMove=$1
        trackToMove=$((trackToMove - 1))
        newTrackPosition=$2
        trackCount=$(getPropertyString playlist-count)

        test "$trackToMove" -lt 0 || test "$trackToMove" -ge "$trackCount" && {
            printf '%s\n' "Item $trackToMove is out of range of playlist." >&2
            exit 1
        }
    }

    test "$newTrackPosition" -lt 0 && {
        printf '%s\n' "Position $newTrackPosition is out of range of playlist." >&2
        exit 1
    }

    test "$newTrackPosition" -eq 1 && newTrackPosition=0

    printf '%s\n' "{ \"command\": [\"playlist-move\", \"$trackToMove\", \
\"$newTrackPosition\" ] }" | $SOCKETCOMMAND > /dev/null

    test "$QUIETFLAG" != "true" && {
        getPlaylist
        QUIETFLAG=true
    }
}

removeTrack() {
    trackToRemove=$1

    test "$trackToRemove" = "current" && {
        printf '%s\n' "{ \"command\": [\"playlist-remove\", \
        \"$trackToRemove\" ] }" | $SOCKETCOMMAND > /dev/null
    } || {
        intCheck "$1" || exit 1

        trackToRemove=$((trackToRemove - 1))
        trackCount=$(getPropertyString playlist-count)

        test "$trackToRemove" -lt 0 || test "$trackToRemove" -ge "$trackCount" && {
            printf '%s\n' "Item $trackToRemove is out of range of playlist." >&2
            exit 1
        }

        filename="$(getPlaylistFilename $trackToRemove)"

        printf '%s\n' "{ \"command\": [\"playlist-remove\", \
        \"$trackToRemove\" ] }" | $SOCKETCOMMAND > /dev/null
    }

    test "$QUIETFLAG" != "true" && {
        printf '%s\n' "$filename has been removed from the playlist."
        exit
    }
}

alwaysPlay() {
    currentTrack=$(getPropertyString playlist-pos)
    test "$currentTrack" = "N/A" && {
        setTrackAbsolute 1
        return
    }

    printf '%s\n' "{ \"command\": [\"set_property\", \"pause\", false ] }" | \
        $SOCKETCOMMAND > /dev/null
}

alwaysPause() {
    status="true"
    printf '%s\n' "{ \"command\": [\"set_property\", \"pause\", $status ] }" | \
        $SOCKETCOMMAND > /dev/null
}

togglePause() {
    currentTrack=$(getPropertyString playlist-pos)
    test "$currentTrack" = "N/A" && {
        setTrackAbsolute 1
        return
    }

    status=$(getPauseStatus)
    test "$status" = "playing" && status="true" || status="false"

    printf '%s\n' "{ \"command\": [\"set_property\", \"pause\", $status ] }" | \
        $SOCKETCOMMAND > /dev/null
}

toggleMute() {
    test -z "$1" && {
        muted=$(getMuteStatus)
        test "$muted" = "no" && muted="true" || muted="false"
    } || {
        muted=$1
    }

    printf '%s\n' "{ \"command\": [\"set_property\", \"mute\", $muted ] }" | \
        $SOCKETCOMMAND > /dev/null
}

toggleLoopFile() {
    test -z "$1" && {
        loopFile=$(getLoopStatus loop-file)
        test "$loopFile" = "no" && loopFile="inf" || loopFile="no"
    } || {
        loopFile=$1
    }

    printf '%s\n' "{ \"command\": [\"set_property_string\", \"loop-file\", \
\"$loopFile\" ] }" | $SOCKETCOMMAND > /dev/null
}

toggleLoopPlaylist() {
    test -z "$1" && {
        loopPlaylist=$(getLoopStatus loop-playlist)
        test "$loopPlaylist" = "no" && loopPlaylist="inf" || loopPlaylist="no"
    } || {
        loopPlaylist=$1
    }

    printf '%s\n' "{ \"command\": [\"set_property_string\", \"loop-playlist\", \
\"$loopPlaylist\" ] }" | $SOCKETCOMMAND > /dev/null
}

shufflePlaylist() {
    printf '%s\n' "{ \"command\": [\"playlist-shuffle\" ] }" | \
        $SOCKETCOMMAND > /dev/null

    test "$QUIETFLAG" != "true" && {
        printf '%s\n' "Playlist shuffled."
        QUIETFLAG=true
    }

    test "$(getLoopStatus loop-playlist)" != "yes" && {
        toggleLoopPlaylist inf
    }
}

cropPlaylist() {
    printf '%s\n' "{ \"command\": [\"playlist-clear\" ] }" | \
        $SOCKETCOMMAND > /dev/null

    test "$QUIETFLAG" != "true" && {
        getPlaylist
        QUIETFLAG=true
    }
}

clearPlaylist() {
    QUIETFLAG=true

    # kill any running process of mpvc add
    pgrep -f "mpvc add" > /dev/null 2>&1 && {
        pkill -f "mpvc add"
    }

    cropPlaylist
    removeTrack 1
    printf '%s\n' "Playlist cleared."
}

# quit mpv process on given socket
killSocket() {
    printf '%s\n' "{ \"command\": [\"quit\"] }" | $SOCKETCOMMAND > /dev/null
    exit
}

# kill all instances of mpv running under your user
killAllMpv() {
    pkill "mpv" /dev/null 2>&1
    exit
}

    # Time Functions
###############################################################################

elapsedTime() {
    time=$(getPropertyString playback-time)
    formatTime "$time"
}

preciseElapsedTime() {
    time=$(getPropertyString playback-time 0.0)
    bigTime=$(printf '%s\n' "$time" | cut -d. -f 1)
    tinyTime=$(printf '%s\n' "$time" | cut -d. -f 2)
    formatTime "$bigTime" "$tinyTime"
}

trackLength() {
    duration=$(getPropertyString duration)
    formatTime "$duration"
}

playtimeRemaining() {
    playtime=$(getPropertyString playtime-remaining)
    formatTime "$playtime"
}

# format seconds into HH:MM:SS format
formatTime() {
    test "$1" = "N/A" && return 1

    rawSeconds=$1
    seconds=$((rawSeconds % 60))
    minutes=$((rawSeconds / 60))
    hours=$((minutes / 60))

    test $seconds -lt 10 && seconds="0$seconds"
    test $minutes -ge 60 && minutes=$((minutes - hours*60))
    test $minutes -lt 10 && minutes="0$minutes"
    test $hours -lt 10 && hours="0$hours"

    test -z "$2" && {
        printf '%s\n' "$hours:$minutes:$seconds"
    } || {
        milleSeconds=$2
        printf '%s\n' "$hours:$minutes:$seconds.$milleSeconds"
    }
}

    # Formatting and Printing Functions
###############################################################################

# formats and prints according to $FORMATSTRING
formatPrint() {
    # modified format string
    FORMATSTRING=$(printf '%s\n' "$FORMATSTRING" | sed "
    s#%status%#$(getPauseStatus)#g
    s#%year%#$(getMetadata date)#g
    s#%genre%#$(getMetadata genre)#g
    s#%title%#$(getMetadata title)#g
    s#%album%#$(getMetadata album)#g
    s#%artist%#$(getMetadata artist)#g
    s#%comment%#$(getMetadata comment)#g
    s#%albumartist%#$(getMetadata album_artist)#g
    s#%speed%#$(getSpeed)#g
    s#%time%#$(elapsedTime)#g
    s#%precisetime%#$(preciseElapsedTime)#g
    s#%volume%#$(getPropertyString volume)#g
    s#%length%#$(trackLength)#g
    s#%remaining%#$(playtimeRemaining)#g
    s#%muted%#$(getMuteStatus)#g
    s#%percentage%#$(getPropertyString percent-pos)#g
    s#%name%#$(getMediaProperties filename)#g
    s#%path%#$(getMediaProperties path)#g
    s#%dir%#$(getPropertyString working-directory)#g
    s#%repeat%#$(getLoopStatus loop-playlist)#g
    s#%single%#$(getLoopStatus loop-file)#g
    s#%playlistlength%#$(getPropertyString playlist-count)#g
    s#%position%#$(($(getPropertyString playlist-pos) + 1))#g
    s#%frame%#$(getProperty estimated-frame-number)#g
    s#%width%#$(getProperty width)#g
    s#%height%#$(getProperty height)#g
    s#%ab-loop-a%#$(getProperty ab-loop-a)#g
    s#%ab-loop-b%#$(getProperty ab-loop-b)#g
    ")

    printf '%s\n' "${FORMATSTRING}"
    exit
}

# print default status of mpv instance
printDefaultStatus() {
    artist=$(getMetadata artist)

    test "$artist" != "N/A" && {
        FORMATSTRING="\
${artist} - %title%
[%status%] #%position%/%playlistlength% %time%/%length% (%percentage%%)
speed: %speed%x volume: %volume%% muted: %muted% repeat: %repeat% single: %single%"
    } || {
        FORMATSTRING="\
%title%
[%status%] #%position%/%playlistlength% %time%/%length% (%percentage%%)
speed: %speed%x volume: %volume%% muted: %muted% repeat: %repeat% single: %single%"
    }

    formatPrint
}

printPrettyOutput() {
    exit 0
}

# catches if mpv is idle or not
printFinalOutput() {
    test "$QUIETFLAG" != "true" && {
        test "$(getPropertyString idle-active)" = "yes" && {
            printf '%s\n' "MPV instance on ${SOCKET} is currently idle." >&2
        } || {
            printDefaultStatus
        }
    }

    exit
}

    # Misc. Functions
###############################################################################

escapeSedChar() {
    awk -F "$1" '{
        for (x=1; x<NF; x++) printf "%s\\'"$1"'", $x
        printf "%s", $NF
        exit }'
}

intCheck() {
    test "$1" -ne 0 2> /dev/null
    test "$?" -ne 2 || return 1
}

fltCheck() {
    intCheck "$1" && return 0

    case "$1" in
        *.*.*|*[!-.0-9]*) ;;
        *[0-9].[0-9]*) return 0 ;;
    esac

    return 1
}

calculateTerminalHeight() {
    rows=$(($(tput lines) - 2))
    halfrows=$((rows / 2))

    test "$tracks" -gt $rows && {
        test "$currentTrack" -le $halfrows && {
            FIRST=1
            LAST=$rows
            return
        }
        test $((currentTrack + halfrows)) -ge "$tracks" && {
            FIRST=$((tracks - rows + 1))
            LAST=$tracks
            return
        }
        test $((currentTrack + halfrows)) -lt "$tracks" && {
            FIRST=$((currentTrack - halfrows + 1))
            LAST=$((currentTrack + halfrows))
            return
        }
    }

    FIRST=1
    LAST=$tracks
}

parseTimeString() {
    printf '%s' "$1" | grep -q -e "^\([0-9.]*:\)\{0,2\}[0-9.]*$" -e "^[0-9]*[sSmMhH]$" || {
        cat >&2 << EOF
Timestamp formats must match either H:M:S with hour and minute fields optional,
or a single integer number with a unit of time appended: h, m, s.
EOF
        exit 1
    }

    lastChar=$(printf '%s\n' "$1" | rev | cut -c 1)

    case "$lastChar" in
        s|S|m|M|h|H)
            timeInt=$(printf '%s' "$1" | rev | cut -c 2- | rev)

            intCheck "$timeInt" || exit 1

            case "$lastChar" in
                h|H) timeInt=$((timeInt * 60 * 60)) ;;
                m|M) timeInt=$((timeInt * 60))      ;;
            esac

            ;;
        *)
            timeInt=$(printf '%s' "$1" | awk -F ':' '{
                time=0
                for (x=1; x<=NF; x++) time=time*60+$x
                    printf "%d\n", time
                    exit
            }')

            ;;
    esac

    printf "%d" "$timeInt"
}

validateDeps() {
    type mpv > /dev/null 2>&1 || {
        printf '%s\n' "Cannot find mpv on your \$PATH." >&2
        exit 4
    }

    type nc > /dev/null 2>&1 && SOCKETCOMMAND="nc -U -N $SOCKET"
    type socat > /dev/null 2>&1 && SOCKETCOMMAND="socat - $SOCKET"

    test "$SOCKETCOMMAND" || {
        printf '%s\n' "Cannot find socat or nc on your \$PATH." >&2
        exit 4
    }

    type jot > /dev/null 2>&1 && SEQCOMMAND="jot"
    type seq > /dev/null 2>&1 && SEQCOMMAND="seq"

    test "$SEQCOMMAND" || {
        printf '%s\n' "Cannot find seq or jot on your \$PATH." >&2
        exit 4
    }
}

validateBC() {
    type bc > /dev/null 2>&1 || {
        printf '%s\n' "Cannot find bc on your \$PATH."
        printf '%s\n' "Please install for speed control."
        exit 4
    }
}

validateSocket() {
    test "$PLAYFLAG" != "true" && {
        # test if socket exists
        test -S "$SOCKET" || {
            printf '%s\n' "$SOCKET does not exist. Use mpv --input-ipc-server to start one." >&2
            exit 2
        }

        # test if socket is open
        status="$(getPauseStatus)"
        test "$status" = "N/A" && {
            printf '%s\n' "No files added to $SOCKET."
            exit 3
        }
    } || {
        return
    }
}

getVersion() {
    mpv --version

    printf '%s\n' "MPVC Release 1.3 (c) Laurence Willetts MIT"
    exit
}

main() {
    validateDeps

    # grab mpv options first if any
    for arg in "$@"; do
        test "$MPVFLAG" = "true" && {
            MPVOPTIONS="$MPVOPTIONS $arg"
        } || {
            test "$arg" = "--" && MPVFLAG=true
        }
    done

    # grab input files next
    for arg in "$@"; do
        case "$arg" in
            --) break ;;
            -?|--*) APPENDFLAG=false ;;
        esac

        test "$APPENDFLAG" = "true" && {
            APPENDED=true
            QUIETFLAG=true
            appendTrack "$arg"
        }

        case "$arg" in
            add|-a|--append) APPENDFLAG=true ;;
        esac
    done

    # grab piped input if no files were passed and we're expecting data on stdin
    test -z "$APPENDED" &&
        { test "$APPENDFLAG" = "true" || test -p /dev/stdin ; } && {
        QUIETFLAG=true
        while read -r line; do
            appendTrack "${line}"
        done
    }

    validateSocket

    # mpc compatibility layer
    case "$1" in
        play|start|resume)
            case "$2" in
                0)
                    setTrackAbsolute 1
                    ;;
                $)
                    setTrackAbsolute $(getPropertyString playlist-count)
                    ;;
                *)
                    intCheck "$2" && setTrackAbsolute "$2"
                    ;;
            esac

            alwaysPlay
            printFinalOutput
            ;;
        vol|volume)
            firstChar=$(printf '%s\n' "$2" | cut -c 1)
            case "$firstChar" in
                "+")
                    volume=$(printf '%s\n' "$2" | cut -d+ -f2)
                    setVolumeRelative "$volume"
                    ;;
                "-")
                    volume=$(printf '%s\n' "$2" | cut -d- -f2)
                    setVolumeRelative "-${volume}"
                    ;;
                *)
                    test ! -z "$2" && {
                        setVolumeAbsolute "$2"
                    } || {
                        printf '%s\n' "Specify volume in/decrease amount or absolute amount."
                        return 1
                    }
                    ;;
            esac

            printFinalOutput
            ;;
        repeat)
            case "$2" in
                "on")  toggleLoopPlaylist inf ;;
                "off") toggleLoopPlaylist no  ;;
                *)     toggleLoopPlaylist     ;;
            esac

            printFinalOutput
            ;;
        single)
            case "$2" in
                "on")  toggleLoopFile yes ;;
                "off") toggleLoopFile no  ;;
                *)     toggleLoopFile     ;;
            esac

            printFinalOutput
            ;;
        metadata) getFilenameMetadata "$2" ;;
        pause)    alwaysPause         ; printFinalOutput ;;
        next)     setTrackRelative 1  ; printFinalOutput ;;
        prev)     setTrackRelative -1 ; printFinalOutput ;;
        move)     moveTrack "$2" "$3" ; printFinalOutput ;;
        mute)     toggleMute true     ; printFinalOutput ;;
        unmute)   toggleMute false    ; printFinalOutput ;;
        pretty)   printPrettyOutput ;;
        # find) do fuzzy search
        # idleloop) observe_property
        # consume) to implement # add on|off toggle
        # random) MPV doesn't have this control option!
        # create an issue or implement puesdo-random tracks
    esac

    # GNU-style options
    for arg in "$@"; do
        test "$SEEKFLAG"     = "true" && setTimeRelative "$arg";   SEEKFLAG=false
        test "$TIMEFLAG"     = "true" && setTimeAbsolute "$arg";   TIMEFLAG=false
        test "$VOLFLAG"      = "true" && setVolumeRelative "$arg"; VOLFLAG=false
        test "$VOLUMEFLAG"   = "true" && setVolumeAbsolute "$arg"; VOLUMEFLAG=false
        test "$SPEEDFLAG"    = "true" && setSpeedRelative "$arg";  SPEEDFLAG=false
        test "$SPEEDVALFLAG" = "true" && setSpeedAbsolute "$arg";  SPEEDVALFLAG=false
        test "$TRACKFLAG"    = "true" && setTrackRelative "$arg";  TRACKFLAG=false
        test "$TRACKVALFLAG" = "true" && setTrackAbsolute "$arg";  TRACKVALFLAG=false
        test "$REMOVEFLAG"   = "true" && removeTrack "$arg";       REMOVEFLAG=false
        test "$SAVEFLAG"     = "true" && savePlaylist "$arg";      SAVEFLAG=false

        case "$arg" in
            -t|--seek|seek)                  SEEKFLAG=true                  ;;
            -T|--time)                       TIMEFLAG=true                  ;;
            -v|--vol)                        VOLFLAG=true                   ;;
            -V|--volume)                     VOLUMEFLAG=true                ;;
            -x|--speed|speed)                SPEEDFLAG=true                 ;;
            -X|--speedval)                   SPEEDVALFLAG=true              ;;
            -j|--track)                      TRACKFLAG=true                 ;;
            -J|--tracknum)                   TRACKVALFLAG=true              ;;
            -r|--remove|rm|remove|del)       REMOVEFLAG=true                ;;
            --save|save)                     SAVEFLAG=true                  ;;
            -s|--stop|stop)                  alwaysPause; setTimeAbsolute 0 ;;
            -P|--play|play)                  alwaysPlay                     ;;
            -p|--toggle|toggle)              togglePause                    ;;
            -m|--mute)                       toggleMute                     ;;
            mute)                            toggleMute true                ;;
            unmute)                          toggleMute false               ;;
            -i|--playlist|playlist)          getPlaylist; exit              ;;
            -I|--fullplaylist|fullplaylist)  getFullPlaylist; exit          ;;
            -L|--loop|loop)                  toggleLoopPlaylist             ;;
            -l|--loopfile|loopfile)          toggleLoopFile                 ;;
            -z|--shuffle|shuffle)            shufflePlaylist                ;;
            -c|--crop|crop)                  cropPlaylist                   ;;
            -C|--clear|clear)                clearPlaylist                  ;;
            -k|--kill|kill)                  killSocket                     ;;
            --version|version)               getVersion                     ;;
            -f|--format)                     continue                       ;;
            -a|--append)                     continue                       ;;
            -S|--socket)                     continue                       ;;
            -[1-9][0-9][0-9][0-9][0-9][0-9]) continue                       ;;
            --|---|----)                     continue                       ;;
            -?)                              usage 1                        ;;
        esac
    done

    # produce format strings last
    test "$QUIETFLAG" != "true" && {
        test ! -z "$FORMATSTRING" && formatPrint
        printFinalOutput
    }
}

# more global argument parsing
for arg in "$@"; do
    test "$SOCKETFLAG" = "true" && SOCKET=$arg && SOCKETFLAG=false
    test "$FORMATFLAG" = "true" && FORMATSTRING=$arg && FORMATFLAG=false

    case $arg in
        --version)        getVersion                        ;;
        -q|--quiet)       QUIETFLAG=true                    ;;
        -I|--image)       IMAGEFLAG=true                    ;;
        -S|--socket)      SOCKETFLAG=true                   ;;
        -f|--format)      FORMATFLAG=true                   ;;
        -Q|--vid=no)      MPVOPTIONS="$MPVOPTIONS --vid=no" ;;
        -K|--killall)     killAllMpv                        ;;
        --list-options)   usage 0                           ;;
        -h|--help|h|help) usage 0                           ;;
    esac
done

test "$QUIETFLAG" = "true" && {
    main "$@" >/dev/null 2>&1
} || {
    main "$@"
}
