Skip to content

Instantly share code, notes, and snippets.

@jfharden
Created May 31, 2021 22:04
Show Gist options
  • Save jfharden/aa34fbd0a2fba67f11e501633afcfd68 to your computer and use it in GitHub Desktop.
Save jfharden/aa34fbd0a2fba67f11e501633afcfd68 to your computer and use it in GitHub Desktop.
Script to convert m4v to mkv files and maintain audio/subtitle stream names
#!/bin/bash
# Requirements to use this script:
# HandBrakeCLI
# jq
# bash 4+ (sorry osx!)
# ffmpeg
set -e
# Default to 1 if DRY_RUN is either unset or empty
DRY_RUN=${DRY_RUN:-1}
INPUT_FILE="$1"
function usage {
echo "Usage:"
echo " $0 <input_file>"
echo
echo "Note: Environment variable DRY_RUN must be set to 0 in order to actually run the conversion."
echo "Not setting DRY_RUN to 0 will cause the command line will just to be printed and not executed"
echo
echo "Example:"
echo " DRY_RUN=0 $0 ./my_file.m4v"
echo
exit 1
}
if [ $# -ne 1 ]; then
echo "Error: Wrong number of arguments"
echo
usage
fi
if [[ ! "$INPUT_FILE" =~ \.m4v$ ]]; then
echo "Error: Not an m4v file, please specify an m4v file as the input file"
echo
usage
fi
if [ ! -f "$INPUT_FILE" ]; then
echo "Error: Input file '$INPUT_FILE' does not exist, or is not a regular file"
echo
usage
fi
OUTPUT_FILE=$(sed -E 's/.m4v$/.mkv/' <<<"$INPUT_FILE")
echo " Input File: $INPUT_FILE"
echo "Output File: $OUTPUT_FILE"
# The json output from HandbrakeCLI isn't valid json, it's got 2 json blobs each prefixed with some text, the 'JSON
# Title Set' is the one we care about and is the second so we'll use a perl multi-line regex to remove everything up to
# the json blob we care about
INPUT_STREAMS=$(HandBrakeCLI --scan --json --input "$INPUT_FILE" 2>/dev/null | perl -0777 -pe 's/.*JSON Title Set: //gs')
FFMPEG_FLAGS=(
-i "$INPUT_FILE" # Input filename
-codec copy # copy streams without transcoding
-map 0:v # copy all video streams in the same order
-map 0:a # copy all audio streams in the same order
-map 0:s # copy all subtitle streams in the same order
)
# Whan changing track metadata the arguments are constructed as follows:
# -meteadata:<s for stream>:<a for audio,s for subtitle>:<stream_index (0 based)> title="Stream Title"
#
# Examples:
# -metadata:s:a:0 title="Audio Stream Title 1"
# -metadata:s:s:0 title="Subtitle Stream Title 1"
# -metadata:s:a:1 title="Audio Stream Title 2"
# -metadata:s:s:1 title="Subtitle Stream Title 2"
readarray -t AUDIO_NAMES < <(
jq -r '[.TitleList[0].AudioList[].Name] | map(if . == null then . = "" else . end) | .[]' <<< "$INPUT_STREAMS"
)
for STREAM_INDEX in $(seq 0 $((${#AUDIO_NAMES[@]} - 1))); do
STREAM_NAME=${AUDIO_NAMES[$STREAM_INDEX]}
if [ -n "$STREAM_NAME" ]; then
FFMPEG_FLAGS+=(
"-metadata:s:a:$STREAM_INDEX" "title=$STREAM_NAME"
)
fi
done
readarray -t SUBTITLE_NAMES < <(
jq -r '[.TitleList[0].SubtitleList[].Name] | map(if . == null then . = "" else . end) | .[]' <<< "$INPUT_STREAMS"
)
for STREAM_INDEX in $(seq 0 $((${#SUBTITLE_NAMES[@]} - 1))); do
STREAM_NAME=${SUBTITLE_NAMES[$STREAM_INDEX]}
if [ -n "$STREAM_NAME" ]; then
FFMPEG_FLAGS+=(
"-metadata:s:s:$STREAM_INDEX" "title=$STREAM_NAME"
)
fi
done
# # Turns out with HandBrakeCLI it's been setting the default flag on the first sub track even when I've had
# --subtitle-default none so I can't rely on the default flag to tell me a track should be forced.
# DEFAULT_SUBTITLE_TRACK=$(
# jq -r '[.TitleList[0].SubtitleList[]] | map(.Attributes.Default) | index(true) | values' <<< "$INPUT_STREAMS"
# )
# if [ -n "$DEFAULT_SUBTITLE_TRACK" ]; then
# # Plex ignores default flags, and when HandBrakeCli sets a default in mkv it actually sets the forced flag as well as
# # the default, but ffmpeg correctly copies the default when we convert so we just need to apply the forced flag
# # to the default subtitle
# FFMPEG_FLAGS+=(
# "-disposition:s:$DEFAULT_SUBTITLE_TRACK" "forced"
# )
# fi
FFMPEG_FLAGS+=(
"$OUTPUT_FILE"
)
echo ffmpeg "${FFMPEG_FLAGS[@]}"
if [ "$DRY_RUN" -eq 0 ]; then
ffmpeg "${FFMPEG_FLAGS[@]}"
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment