Skip to content

Instantly share code, notes, and snippets.

@fonic
Last active November 20, 2023 08:31
Show Gist options
  • Save fonic/51930c3eb5e1ff56b0d9cf204cae1ea9 to your computer and use it in GitHub Desktop.
Save fonic/51930c3eb5e1ff56b0d9cf204cae1ea9 to your computer and use it in GitHub Desktop.
remove-unneeded-languages.sh - Remove unneeded languages from video files using FFmpeg
#!/usr/bin/env bash
# -------------------------------------------------------------------------
# -
# Remove unneeded language(s) from video file -
# -
# Created by Fonic <https://github.com/fonic> -
# Date: 04/14/22 - 11/20/23 -
# -
# Posted as answer on StackExchange Unix & Linux: -
# https://unix.stackexchange.com/a/760374/399674 -
# -
# Published as GIST: -
# https://gist.github.com/fonic/51930c3eb5e1ff56b0d9cf204cae1ea9 -
# -
# Based on: -
# https://www.reddit.com/r/ffmpeg/comments/r3dccd/how_to_use_ffmpeg_ -
# to_detect_and_delete_all_non/ -
# https://wiki.sharewiz.net/doku.php?id=ffmpeg:remove_video_audio_ -
# streams_by_language -
# -
# -------------------------------------------------------------------------
# --------------------------------------
# Functions -
# --------------------------------------
# Print normal/hilite/good/warn/error message [$*: message]
function printn() { echo -e "$*"; }
function printh() { echo -e "\e[1m$*\e[0m"; }
function printg() { echo -e "\e[1;32m$*\e[0m"; }
function printw() { echo -e "\e[1;33m$*\e[0m" >&2; }
function printe() { echo -e "\e[1;31m$*\e[0m" >&2; }
# Print command [$1: command, $2..$n: arguments]
function print_cmd() {
local output="Command:" arg
for arg; do
output+=" "
if [[ "${arg}" =~ ^(--.+)=(.+)$ ]]; then
output+="${BASH_REMATCH[1]}="
arg="${BASH_REMATCH[2]}"
fi
if [[ "${arg}" == *[[:space:]]* || "${arg}" == *"$"* || "${arg}" == *";"* || "${arg}" == *"&"* || \
"${arg}" == *"\`"* || "${arg}" == *"~"* || "${arg}" == *"{"* || "${arg}" == *"}"* || \
"${arg}" == *"\""* || "${arg}" == *"#"* || "${arg}" == *"("* || "${arg}" == *")"* || \
"${arg}" == *"\\"* || "${arg}" == *"'"* || "${arg}" == *"<"* || "${arg}" == *">"* || \
"${arg}" == *"*"* || "${arg}" == *"?"* ]]; then
arg="${arg//\\/\\\\}"; arg="${arg//\$/\\\$}" # [\$"`] need to be escaped as those still
arg="${arg//\"/\\\"}"; arg="${arg//\`/\\\`}" # have a special meaning even within "..."
output+="\"${arg}\""
else
output+="${arg}"
fi
done
printn "${output}"
}
# --------------------------------------
# Main -
# --------------------------------------
# Set up error handling
set -ue; trap "printe \"Error: an unhandled error occurred on line \${LINENO}, aborting.\"; exit 1" ERR
# Process command line
if (( $# != 3 )); then
printn "\e[1mUsage:\e[0m ${0##*/} LANGUAGES INFILE OUTFILE"
printn "\e[1mExample:\e[0m ${0##*/} \"eng,spa?\" video.mkv video_2.mkv"
printn "\e[1mExample:\e[0m for file in *.mkv; do ${0##*/} \"eng,spa?\" \"\${file}\" \"out/\${file}\"; done"
printn "\e[1mNote:\e[0m LANGUAGES specifies language(s) to KEEP (comma-separated list; add a"
printn "\e[1m\e[0m trailing '?' to denote OPTIONAL languages, i.e. no error if missing)"
exit 2
fi
IFS="," read -a langs -r <<< "$1"
infile="$2"
outfile="$3"
# Sanity checks
[[ -f "${infile}" ]] || { printe "Error: input file '${infile}' does not exist, aborting."; exit 1; }
[[ ! -f "${outfile}" ]] || { printe "Error: output file '${outfile}' already exists, aborting."; exit 1; }
command -v "ffmpeg" >/dev/null || { printe "Error: required command 'ffmpeg' is not available, aborting."; exit 1; }
# Run ffmpeg:
# -i <file> specify input file (identified as '0:' in mappings below)
# -map 0:v map all video streams from input to output file
# -map 0:m:language:<lang>[?] map all streams of specified language from input to output file
# (option may appear multiple times to specify multiple languages;
# trailing '?' means 'optional', i.e. no error if input file does
# not contain any tracks for specified language)
# -codec copy copy streams as they are without reencoding
# -loglevel warning output warning and error messages (but no info messages)
printh "Processing file '${infile}'..."
opts=()
opts+=("-i" "${infile}")
opts+=("-map" "0:v")
for lang in "${langs[@]}"; do opts+=("-map" "0:m:language:${lang}"); done
opts+=("-codec" "copy")
opts+=("-loglevel" "warning")
opts+=("${outfile}")
print_cmd "ffmpeg" "${opts[@]}"
ffmpeg "${opts[@]}" || { printe "Error: ffmpeg failed to process file '${infile}'"; exit 1; }
exit 0
@fonic
Copy link
Author

fonic commented Nov 20, 2023

Usage information:

Usage:   remove-unneeded-languages.sh LANGUAGES INFILE OUTFILE
Example: remove-unneeded-languages.sh "eng,spa?" video.mkv video_2.mkv
Example: for file in *.mkv; do remove-unneeded-languages.sh "eng,spa?" "${file}" "out/${file}"; done
Note:    LANGUAGES specifies language(s) to KEEP (comma-separated list; add a
         trailing '?' to denote OPTIONAL languages, i.e. no error if missing)

Example:
Process all .mkv files in the current directory, remove all languages except English and Spanish (whereas Spanish is optional, i.e. no error if a video file does not contain any tracks for Spanish), write output files to subfolder out:

mkdir out; for file in *.mkv; do remove-unneeded-languages.sh "eng,spa?" "${file}" "out/${file}"; done

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment