From b3070cc6588c1ed3bab05b8413e93c9b834881d7 Mon Sep 17 00:00:00 2001 From: Thorsten Date: Tue, 3 Feb 2026 12:49:46 +0100 Subject: [PATCH] . --- auto_add_sub_folder.desktop | 18 ++ bin/auto_add_sub_folder.sh | 430 ++++++++++++++++++++++++++++++++++++ bin/auto_sort.sh | 14 +- 3 files changed, 456 insertions(+), 6 deletions(-) create mode 100755 auto_add_sub_folder.desktop create mode 100755 bin/auto_add_sub_folder.sh diff --git a/auto_add_sub_folder.desktop b/auto_add_sub_folder.desktop new file mode 100755 index 0000000..31b7c6c --- /dev/null +++ b/auto_add_sub_folder.desktop @@ -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= diff --git a/bin/auto_add_sub_folder.sh b/bin/auto_add_sub_folder.sh new file mode 100755 index 0000000..9d120c1 --- /dev/null +++ b/bin/auto_add_sub_folder.sh @@ -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:-}" + + 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 "$@" diff --git a/bin/auto_sort.sh b/bin/auto_sort.sh index 2d07244..f2db1f4 100755 --- a/bin/auto_sort.sh +++ b/bin/auto_sort.sh @@ -139,12 +139,14 @@ main() { done < <( find "$input_folder" \ - -type d -regex "^$escaped_output_dir$" -prune -false -o \ - -type d -regex "^$escaped_output_dir/.*" -prune -false -o \ - -type f \( \ - -iname "*.mkv" -o -iname "*.mp4" -o -iname "*.avi" -o \ - -iname "*.ts" -o -iname "*.vob" -o -iname "*.mpeg" -o -iname "*.mov" \ - \) -print + -type d -regex "^$escaped_output_dir$" -prune -false -o \ + -type d -regex "^$escaped_output_dir/.*" -prune -false -o \ + -type f \( \ + -iname "*.mkv" -o -iname "*.mp4" -o -iname "*.avi" -o \ + -iname "*.ts" -o -iname "*.vob" -o -iname "*.mpeg" -o -iname "*.mov" \ + \) \ + ! -iname "*sample.*" \ + -print ) if [ "$found_any" = false ]; then