This commit is contained in:
2026-02-03 12:49:46 +01:00
parent 3fab153496
commit b3070cc658
3 changed files with 456 additions and 6 deletions

18
auto_add_sub_folder.desktop Executable file
View File

@@ -0,0 +1,18 @@
[Desktop Entry]
Comment[de_DE]=
Comment=
Exec=sh -e -c 'exec "$(dirname "$0")/bin/auto_add_sub_folder.sh"' %k
GenericName[de_DE]=
GenericName=
Icon=mkvmerge
MimeType=
Name[de_DE]=auto_add_sub_folder
Name=auto_add_sub_folder
Path=
PrefersNonDefaultGPU=false
StartupNotify=true
Terminal=true
TerminalOptions=
Type=Application
X-KDE-SubstituteUID=false
X-KDE-Username=

430
bin/auto_add_sub_folder.sh Executable file
View File

@@ -0,0 +1,430 @@
#!/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 "$@"

View File

@@ -139,12 +139,14 @@ main() {
done < <( done < <(
find "$input_folder" \ find "$input_folder" \
-type d -regex "^$escaped_output_dir$" -prune -false -o \ -type d -regex "^$escaped_output_dir$" -prune -false -o \
-type d -regex "^$escaped_output_dir/.*" -prune -false -o \ -type d -regex "^$escaped_output_dir/.*" -prune -false -o \
-type f \( \ -type f \( \
-iname "*.mkv" -o -iname "*.mp4" -o -iname "*.avi" -o \ -iname "*.mkv" -o -iname "*.mp4" -o -iname "*.avi" -o \
-iname "*.ts" -o -iname "*.vob" -o -iname "*.mpeg" -o -iname "*.mov" \ -iname "*.ts" -o -iname "*.vob" -o -iname "*.mpeg" -o -iname "*.mov" \
\) -print \) \
! -iname "*sample.*" \
-print
) )
if [ "$found_any" = false ]; then if [ "$found_any" = false ]; then