431 lines
12 KiB
Bash
Executable File
431 lines
12 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -Eeuo pipefail
|
|
shopt -s nullglob
|
|
|
|
############################################
|
|
# auto_add_sub_folder.sh
|
|
#
|
|
# Reads input_folder from ../settings.ini (relative to this script in bin/)
|
|
# - If CLI args are provided, they override input_folder.
|
|
#
|
|
# Behavior:
|
|
# - Recursively finds *.mkv under root(s) (case-insensitive)
|
|
# - Robust sample exclusion (case-insensitive):
|
|
# * filename is "sample.mkv"
|
|
# * OR filename contains token "sample" (basename-sample.mkv, basename.Sample.mkv, basename_sample.mkv, etc.)
|
|
# * OR ANY directory component equals "sample" (sample/, SAMPLE/, SaMPle/)
|
|
#
|
|
# - For each MKV: finds a sibling subtitle folder (case-insensitive) via candidates list:
|
|
# Subs, Sub, Subtitles, Subtitle
|
|
# then muxes ALL .idx/.sub pairs found inside.
|
|
#
|
|
# - Does NOT override language (VobSub language usually lives in the .idx)
|
|
# - Sets track-name (heuristic) + forced flag based on idx filename only
|
|
#
|
|
# mkvmerge:
|
|
# - auto-detect native OR flatpak (org.bunkus.mkvtoolnix-gui etc.)
|
|
#
|
|
# Safety:
|
|
# - Writes output to temp file then replaces original
|
|
# - Keeps .bak by default (disable via --no-bak)
|
|
############################################
|
|
|
|
# ───────────────────────── colors ─────────────────────────
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;92m'
|
|
BLUE='\033[0;94m'
|
|
WHITE_ON_GRAY='\033[0;37;100m'
|
|
BLACK_ON_WHITE='\033[0;30;47m'
|
|
WHITE_ON_RED='\033[0;37;41m'
|
|
NC='\033[0m' # No Color
|
|
|
|
DRY_RUN=0
|
|
KEEP_BAK=1
|
|
VERBOSE=1
|
|
|
|
# Robust subtitle folder detection (case-insensitive)
|
|
SUBDIR_CANDIDATES=("Subs" "Sub" "Subtitles" "Subtitle")
|
|
|
|
MKVMERGE_CMD=()
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage:
|
|
auto_add_sub_folder.sh [options] [root] [root2 ...]
|
|
|
|
If no root is given:
|
|
- reads [pathes] input_folder from ../settings.ini
|
|
|
|
Options:
|
|
-n, --dry-run Print commands only, do not execute
|
|
--no-bak Do not keep .bak of original MKV
|
|
-q, --quiet Less logging
|
|
-h, --help Show this help
|
|
EOF
|
|
}
|
|
|
|
log() { (( VERBOSE == 1 )) && printf '[%s] %s\n' "$(date +'%F %T')" "$*"; }
|
|
dbg() { (( VERBOSE == 1 )) && printf ' %s\n' "$*"; }
|
|
die() { printf 'ERROR: %s\n' "$*" >&2; exit 1; }
|
|
|
|
# ---------------- INI parsing (minimal, safe for values with spaces) ----------------
|
|
# Reads first matching key in a section:
|
|
# [section]
|
|
# key=value
|
|
ini_get() {
|
|
local ini_file="$1"
|
|
local section="$2"
|
|
local key="$3"
|
|
|
|
awk -v sec="[$section]" -v key="$key" '
|
|
function ltrim(s){ sub(/^[ \t\r\n]+/, "", s); return s }
|
|
function rtrim(s){ sub(/[ \t\r\n]+$/, "", s); return s }
|
|
function trim(s){ return rtrim(ltrim(s)) }
|
|
BEGIN{ insec=0 }
|
|
{
|
|
line=$0
|
|
# strip BOM if present
|
|
sub(/^\xef\xbb\xbf/, "", line)
|
|
|
|
# ignore comments (full-line)
|
|
if (line ~ /^[ \t]*#/) next
|
|
if (line ~ /^[ \t]*;/) next
|
|
|
|
# section header
|
|
if (match(line, /^[ \t]*\[[^]]+\][ \t]*$/)) {
|
|
insec = (trim(line) == sec)
|
|
next
|
|
}
|
|
|
|
if (!insec) next
|
|
|
|
# key=value, keep everything after first '=' (may contain spaces)
|
|
if (match(line, /^[ \t]*[^=]+=/)) {
|
|
split(line, a, "=")
|
|
k = trim(a[1])
|
|
if (k != key) next
|
|
|
|
pos = index(line, "=")
|
|
v = substr(line, pos+1)
|
|
v = trim(v)
|
|
|
|
# remove optional surrounding quotes
|
|
if (v ~ /^".*"$/) { sub(/^"/, "", v); sub(/"$/, "", v) }
|
|
print v
|
|
exit
|
|
}
|
|
}
|
|
' "$ini_file"
|
|
}
|
|
|
|
# ---------------- mkvmerge detection ----------------
|
|
detect_mkvmerge() {
|
|
log "Detecting mkvmerge…"
|
|
if command -v mkvmerge >/dev/null 2>&1; then
|
|
MKVMERGE_CMD=(mkvmerge)
|
|
log "Using native mkvmerge: $(command -v mkvmerge)"
|
|
return 0
|
|
fi
|
|
|
|
if command -v flatpak >/dev/null 2>&1; then
|
|
local candidates=(
|
|
"org.bunkus.mkvtoolnix-gui"
|
|
"org.bunkus.mkvtoolnix.MKVToolNix"
|
|
"org.bunkus.mkvtoolnix"
|
|
)
|
|
|
|
local appid="" c=""
|
|
for c in "${candidates[@]}"; do
|
|
if flatpak info "$c" >/dev/null 2>&1; then
|
|
appid="$c"
|
|
break
|
|
fi
|
|
done
|
|
|
|
if [[ -z "$appid" ]]; then
|
|
appid="$(flatpak list --app --columns=application 2>/dev/null | grep -i 'mkvtoolnix' | head -n 1 || true)"
|
|
fi
|
|
|
|
if [[ -n "$appid" ]]; then
|
|
MKVMERGE_CMD=(flatpak run --command=mkvmerge "$appid")
|
|
log "Using mkvmerge via Flatpak: $appid"
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
die "Neither native mkvmerge nor a MKVToolNix Flatpak installation was found."
|
|
}
|
|
|
|
# ---------------- sample exclusion ----------------
|
|
# Returns 0 if path should be excluded as sample; otherwise 1.
|
|
is_sample_mkv() {
|
|
local f="$1"
|
|
local base bn lc
|
|
base="$(basename "$f")"
|
|
bn="${base%.*}"
|
|
lc="${bn,,}"
|
|
|
|
# 1) exactly "sample"
|
|
if [[ "$lc" == "sample" ]]; then
|
|
return 0
|
|
fi
|
|
|
|
# 2) token "sample" in basename (separators: dot, underscore, hyphen, space)
|
|
if [[ "$lc" =~ (^|[._\ \-])sample([._\ \-]|$) ]]; then
|
|
return 0
|
|
fi
|
|
|
|
# 3) any directory component equals "sample" (case-insensitive)
|
|
local d seg
|
|
d="$(dirname "$f")"
|
|
while [[ "$d" != "/" && -n "$d" ]]; do
|
|
seg="$(basename "$d")"
|
|
if [[ "${seg,,}" == "sample" ]]; then
|
|
return 0
|
|
fi
|
|
d="$(dirname "$d")"
|
|
done
|
|
|
|
return 1
|
|
}
|
|
|
|
# ---------------- subtitle folder finder (case-insensitive candidates) ----------------
|
|
find_subs_dir() {
|
|
local mkv="$1"
|
|
local dir cand hit
|
|
dir="$(dirname "$mkv")"
|
|
|
|
for cand in "${SUBDIR_CANDIDATES[@]}"; do
|
|
hit="$(find "$dir" -maxdepth 1 -mindepth 1 -type d -iname "$cand" -print -quit 2>/dev/null || true)"
|
|
if [[ -n "$hit" ]]; then
|
|
echo "$hit"
|
|
return 0
|
|
fi
|
|
done
|
|
|
|
return 1
|
|
}
|
|
|
|
# ---------------- subtitle meta (no language override) ----------------
|
|
# Echoes: track_name|forcedFlag
|
|
infer_meta_for_idx() {
|
|
local idx="$1"
|
|
local stem lc forced name
|
|
|
|
stem="$(basename "$idx" .idx)"
|
|
lc="${stem,,}"
|
|
|
|
forced="no"
|
|
[[ "$lc" == *forced* ]] && forced="yes"
|
|
|
|
# Track name heuristic (display only, not language)
|
|
if [[ "$lc" =~ (^|[._\ \-])eng([._\ \-]|$) ]] || [[ "$lc" == *english* ]]; then
|
|
name="English"
|
|
elif [[ "$lc" =~ (^|[._\ \-])de([._\ \-]|$) ]] || [[ "$lc" == *german* ]] || [[ "$lc" =~ (^|[._\ \-])ger([._\ \-]|$) ]] || [[ "$lc" == *deu* ]]; then
|
|
name="Deutsch"
|
|
else
|
|
name="$stem"
|
|
fi
|
|
|
|
[[ "$forced" == "yes" ]] && name+=" (Forced)"
|
|
echo "${name}|${forced}"
|
|
}
|
|
|
|
# ---------------- mux one mkv ----------------
|
|
mux_one_mkv() {
|
|
local mkv="$1"
|
|
local subs
|
|
|
|
log "MKV found: $mkv"
|
|
|
|
subs="$(find_subs_dir "$mkv" || true)"
|
|
dbg "Subs dir : ${subs:-<not found>}"
|
|
|
|
if [[ -z "${subs:-}" ]]; then
|
|
dbg "No subtitles folder found (tried: ${SUBDIR_CANDIDATES[*]}) -> skip"
|
|
return 0
|
|
fi
|
|
|
|
local -a idxs=()
|
|
local f=""
|
|
while IFS= read -r -d '' f; do
|
|
idxs+=("$f")
|
|
done < <(find "$subs" -maxdepth 1 -type f -iname "*.idx" -print0)
|
|
|
|
if [[ ${#idxs[@]} -eq 0 ]]; then
|
|
dbg "Subtitle folder exists but no .idx files -> skip"
|
|
return 0
|
|
fi
|
|
|
|
dbg "IDX files: ${#idxs[@]}"
|
|
for f in "${idxs[@]}"; do dbg " - $(basename "$f")"; done
|
|
|
|
local out_tmp="${mkv%.*}.with-subs.tmp.mkv"
|
|
local bak="${mkv}.bak"
|
|
|
|
local -a cmd=()
|
|
cmd=("${MKVMERGE_CMD[@]}" -o "$out_tmp" "$mkv")
|
|
|
|
local added=0
|
|
local idx=""
|
|
for idx in "${idxs[@]}"; do
|
|
local sub="${idx%.*}.sub"
|
|
if [[ ! -f "$sub" ]]; then
|
|
dbg "Skip idx (missing paired .sub): $(basename "$idx")"
|
|
continue
|
|
fi
|
|
|
|
local meta name forcedFlag
|
|
meta="$(infer_meta_for_idx "$idx")"
|
|
IFS='|' read -r name forcedFlag <<<"$meta"
|
|
|
|
dbg "Add VobSub: $(basename "$idx") -> name='$name', forced=$forcedFlag"
|
|
|
|
# IMPORTANT: do NOT override language; keep what is in the .idx
|
|
cmd+=( --track-name 0:"$name" )
|
|
if [[ "$forcedFlag" == "yes" ]]; then
|
|
cmd+=( --forced-track 0:yes --default-track 0:no )
|
|
else
|
|
cmd+=( --forced-track 0:no --default-track 0:no )
|
|
fi
|
|
cmd+=( "$idx" )
|
|
|
|
((++added))
|
|
done
|
|
|
|
if [[ $added -eq 0 ]]; then
|
|
dbg "No usable subtitle sets (.idx + .sub) -> nothing to mux"
|
|
return 0
|
|
fi
|
|
|
|
log "Muxing $added subtitle input(s) into: $(basename "$mkv")"
|
|
dbg "Temp out : $out_tmp"
|
|
dbg "Command : ${cmd[*]}"
|
|
|
|
if [[ $DRY_RUN -eq 1 ]]; then
|
|
log "Dry-run: not executing."
|
|
return 0
|
|
fi
|
|
|
|
"${cmd[@]}"
|
|
|
|
[[ -f "$out_tmp" ]] || die "mkvmerge finished but temp output file not found: $out_tmp"
|
|
|
|
if [[ $KEEP_BAK -eq 1 ]]; then
|
|
mv -f -- "$mkv" "$bak"
|
|
mv -f -- "$out_tmp" "$mkv"
|
|
log "Done. Backup kept: $(basename "$bak")"
|
|
else
|
|
mv -f -- "$out_tmp" "$mkv"
|
|
log "Done. Original replaced (no .bak)."
|
|
fi
|
|
}
|
|
|
|
# ---------------- scan root ----------------
|
|
process_root() {
|
|
local root="$1"
|
|
[[ -e "$root" ]] || { log "Root not found: $root"; return 0; }
|
|
|
|
log "Scanning root: $root"
|
|
|
|
local -a mkvs=()
|
|
local f=""
|
|
|
|
while IFS= read -r -d '' f; do
|
|
if is_sample_mkv "$f"; then
|
|
dbg "Skip MKV (sample rule): $f"
|
|
continue
|
|
fi
|
|
mkvs+=("$f")
|
|
done < <(find "$root" -type f -iname "*.mkv" -print0)
|
|
|
|
log "Found MKVs: ${#mkvs[@]} under $root"
|
|
if [[ ${#mkvs[@]} -eq 0 ]]; then
|
|
return 0
|
|
fi
|
|
|
|
local processed=0
|
|
local mkv=""
|
|
for mkv in "${mkvs[@]}"; do
|
|
mux_one_mkv "$mkv"
|
|
((++processed))
|
|
done
|
|
|
|
log "Finished root: $root (processed $processed MKV(s))"
|
|
}
|
|
|
|
# ---------------- main ----------------
|
|
main() {
|
|
ROOTS=()
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
-n|--dry-run) DRY_RUN=1; shift ;;
|
|
--no-bak) KEEP_BAK=0; shift ;;
|
|
-q|--quiet) VERBOSE=0; shift ;;
|
|
-h|--help) usage; exit 0 ;;
|
|
--) shift; break ;;
|
|
-*) die "Unknown option: $1" ;;
|
|
*) ROOTS+=("$1"); shift ;;
|
|
esac
|
|
done
|
|
|
|
local script_dir base_dir ini_file
|
|
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
base_dir="$(cd "$script_dir/.." && pwd)"
|
|
ini_file="$base_dir/settings.ini"
|
|
|
|
# If no CLI roots: read input_folder from settings.ini
|
|
if [[ ${#ROOTS[@]} -eq 0 ]]; then
|
|
if [[ ! -f "$ini_file" ]]; then
|
|
echo
|
|
echo -e "${WHITE_ON_RED} Settingsfile nicht gefunden: $ini_file ${NC}"
|
|
echo
|
|
read -n 1 -s -r -p "Press any key to exit"
|
|
exit 1
|
|
fi
|
|
|
|
local input_folder
|
|
input_folder="$(ini_get "$ini_file" "pathes" "input_folder" || true)"
|
|
|
|
if [[ -z "${input_folder:-}" ]]; then
|
|
echo
|
|
echo -e "${WHITE_ON_RED} input_folder fehlt in settings.ini ([pathes]) ${NC}"
|
|
echo
|
|
read -n 1 -s -r -p "Press any key to exit"
|
|
exit 1
|
|
fi
|
|
|
|
ROOTS=("$input_folder")
|
|
fi
|
|
|
|
detect_mkvmerge
|
|
|
|
echo
|
|
echo "────────────────────────────────────────────────────────────────"
|
|
echo -e "${BLACK_ON_WHITE} Auto Add Sub Folder (VobSub) ${NC}"
|
|
echo "────────────────────────────────────────────────────────────────"
|
|
echo " "
|
|
echo -e "${WHITE_ON_GRAY} Input Folder ${NC} ${ROOTS[*]}"
|
|
echo -e "${WHITE_ON_GRAY} Subs Folders ${NC} ${SUBDIR_CANDIDATES[*]}"
|
|
echo -e "${WHITE_ON_GRAY} MKVMerge ${NC} ${MKVMERGE_CMD[*]}"
|
|
echo -e "${WHITE_ON_GRAY} Dry Run ${NC} ${DRY_RUN}"
|
|
echo -e "${WHITE_ON_GRAY} Keep Backup ${NC} ${KEEP_BAK}"
|
|
echo " "
|
|
echo "────────────────────────────────────────────────────────────────"
|
|
echo
|
|
|
|
local r=""
|
|
for r in "${ROOTS[@]}"; do
|
|
process_root "$r"
|
|
done
|
|
|
|
echo
|
|
echo "────────────────────────────────────────────────────────────────"
|
|
echo -e "${GREEN}✔ Finished${NC}"
|
|
echo
|
|
read -n 1 -s -r -p "Press any key to exit"
|
|
exit 0
|
|
}
|
|
|
|
main "$@"
|