diff --git a/bin/auto_sort.sh b/bin/auto_sort.sh index 0ff2ffa..2d07244 100755 --- a/bin/auto_sort.sh +++ b/bin/auto_sort.sh @@ -1,12 +1,15 @@ #!/bin/bash +# Auto Sort — sortiert Videodateien nach Audio-/Untertitel-Spuren +# mit Erkennung zu langer Ordnernamen → generische Gruppennamen -# global variables -declare -a video_file_list +set -euo pipefail + +# -------------------------------------------------------------------- +# CONFIG / COLORS +# -------------------------------------------------------------------- settings_file="./settings.ini" ffprobe_path="/usr/bin/ffprobe" - -#colors RED='\033[0;31m' GREEN='\033[0;92m' BLUE='\033[0;94m' @@ -15,179 +18,187 @@ WHITE_ON_GRAY='\033[0;37;100m' BLACK_ON_WHITE='\033[0;30;47m' WHITE_ON_RED='\033[0;37;41m' YELLOW_ON_WHITE='\033[0;37;43m' -NC='\033[0m' # No Color +NC='\033[0m' + +declare -A group_map # Map von Kombination → Ordnername +declare -A files_by_group # Map von Kombination → Liste von Dateien +declare -a combinations # Liste aller eindeutigen Kombinationen - - -# Funktion zum Extrahieren der Sprachen und Audio-Codec für alle Audio-Spuren +# -------------------------------------------------------------------- +# FUNKTIONEN +# -------------------------------------------------------------------- extract_audio_languages_and_codecs() { local file=$1 - # Führe ffprobe aus und extrahiere die Streams + local ffprobe_json ffprobe_json=$("$ffprobe_path" -v error -show_streams -of json "$file") - - # Durchlaufe alle Audio-Streams und extrahiere Sprache und Codec - audio_streams=() - while IFS= read -r audio_stream; do - stream_index=$(echo "$audio_stream" | jq -r '.index') - codec_name=$(echo "$audio_stream" | jq -r '.codec_name') - language=$(echo "$audio_stream" | jq -r '.tags.language') - - # Format für jede Audio-Spur: [index-codec_language] + local audio_streams=() + while IFS= read -r s; do + codec_name=$(echo "$s" | jq -r '.codec_name') + language=$(echo "$s" | jq -r '.tags.language // "unknown"') audio_streams+=("[$codec_name"_"$language]") - done < <(echo "$ffprobe_json" | jq -c '.streams[] | select(.codec_type == "audio")') - - # Rückgabe des Arrays der Audio-Streams + done < <(echo "$ffprobe_json" | jq -c '.streams[] | select(.codec_type=="audio")') echo "${audio_streams[@]}" } -# Funktion zum Extrahieren der Sprachen und "forced"-Tags für alle Untertitel-Spuren extract_subtitle_languages_and_forced_tags() { local file=$1 - # Führe ffprobe aus und extrahiere die Streams + local ffprobe_json ffprobe_json=$("$ffprobe_path" -v error -show_streams -of json "$file") - - # Durchlaufe alle Untertitel-Streams und extrahiere Sprache und Forced-Tag - subtitle_streams=() - while IFS= read -r subtitle_stream; do - stream_index=$(echo "$subtitle_stream" | jq -r '.index') - language=$(echo "$subtitle_stream" | jq -r '.tags.language') - # Überprüfe sowohl das Forced-Tag in "tags" als auch das Forced-Flag in "disposition" - forced=$(echo "$subtitle_stream" | jq -r '.tags.forced // empty') - forced_flag=$(echo "$subtitle_stream" | jq -r '.disposition.forced') - - # Wenn das "forced"-Tag existiert, berücksichtige es - if [[ "$forced" == "1" || "$forced_flag" == "1" ]]; then + local subtitle_streams=() + while IFS= read -r s; do + language=$(echo "$s" | jq -r '.tags.language // "unknown"') + forced_flag=$(echo "$s" | jq -r '.disposition.forced // 0') + if [[ "$forced_flag" == "1" ]]; then subtitle_streams+=("[forced_$language]") fi subtitle_streams+=("[$language]") - done < <(echo "$ffprobe_json" | jq -c '.streams[] | select(.codec_type == "subtitle")') - - # Rückgabe des Arrays der Untertitel-Streams + done < <(echo "$ffprobe_json" | jq -c '.streams[] | select(.codec_type=="subtitle")') echo "${subtitle_streams[@]}" } -get_user_variables_from_ini_file() { - - # get absolut path of the ini file - settings_file=$(relativ_to_fullpath "$settings_file") - - # abort script if settings file can't be found - if [ ! -e "$settings_file" ]; then - _exit 1 "Settingsfile not found at: "$settings_file - fi - - # read ini values - input_folder=$(sed -nr "/^\[pathes\]/ { :l /^input_folder[ ]*=/ { s/[^=]*=[ ]*//; p; q;}; n; b l;}" "$settings_file") - output_folder=$(sed -nr "/^\[pathes\]/ { :l /^output_folder[ ]*=/ { s/[^=]*=[ ]*//; p; q;}; n; b l;}" "$settings_file") - jsonfile_path=$(sed -nr "/^\[pathes\]/ { :l /^jsonfile_path[ ]*=/ { s/[^=]*=[ ]*//; p; q;}; n; b l;}" "$settings_file") - mode=$(sed -nr "/^\[mode\]/ { :l /^mode[ ]*=/ { s/[^=]*=[ ]*//; p; q;}; n; b l;}" "$settings_file") - mkvmerge=$(sed -nr "/^\[mkvmerge\]/ { :l /^mkvmerge[ ]*=/ { s/[^=]*=[ ]*//; p; q;}; n; b l;}" "$settings_file") - notification_text=$(sed -nr "/^\[notification_text\]/ { :l /^notification_text[ ]*=/ { s/[^=]*=[ ]*//; p; q;}; n; b l;}" "$settings_file") - nofification_audio=$(sed -nr "/^\[nofification_audio\]/ { :l /^nofification_audio[ ]*=/ { s/[^=]*=[ ]*//; p; q;}; n; b l;}" "$settings_file") - - - -} - relativ_to_fullpath() { local path="$1" + local scriptpath scriptpath="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" - - # Entferne doppelte Slashes, bevor der Pfad weiterverarbeitet wird path=$(echo "$path" | sed 's|/+|/|g') - if [ ! -e "$path" ]; then - pathfull=$(realpath "$(dirname "$scriptpath")/$path" 2>/dev/null) + realpath "$(dirname "$scriptpath")/$path" 2>/dev/null || echo "$path" else - pathfull=$(realpath "$path" 2>/dev/null) + realpath "$path" 2>/dev/null || echo "$path" fi +} - echo "$pathfull" +get_user_variables_from_ini_file() { + settings_file=$(relativ_to_fullpath "$settings_file") + if [ ! -e "$settings_file" ]; then + echo -e "${WHITE_ON_RED} Settingsfile nicht gefunden: $settings_file ${NC}" + exit 1 + fi + input_folder=$(sed -nr "/^\[pathes\]/ { :l /^input_folder[ ]*=/ { s/[^=]*=[ ]*//; p; q;}; n; b l;}" "$settings_file") + output_folder=$(sed -nr "/^\[pathes\]/ { :l /^output_folder[ ]*=/ { s/[^=]*=[ ]*//; p; q;}; n; b l;}" "$settings_file") } - -main(){ - +# -------------------------------------------------------------------- +# MAIN +# -------------------------------------------------------------------- +main() { get_user_variables_from_ini_file output_dir="$input_folder/[0] Sort" - # Entferne doppelte Slashes output_dir=$(echo "$output_dir" | sed 's|//|/|g') - - - echo "────────────────────────────────────────────────────────────────" - echo -e "${BLACK_ON_WHITE} Auto Sort ${NC}" - echo "────────────────────────────────────────────────────────────────" - echo " " - echo -e "${WHITE_ON_GRAY} Input Folder ${NC} ""$input_folder""" - echo -e "${WHITE_ON_GRAY} Output Folder ${NC} ""$output_dir""" - echo " " - echo "────────────────────────────────────────────────────────────────" - - mkdir -p "$output_dir" - # Durchlaufe alle Videodateien im Eingabeverzeichnis rekursiv - find "$input_folder" -type f \( -iname "*.mp4" -o -iname "*.mkv" -o -iname "*.avi" \) | while IFS= read -r file; do + echo "──────────────────────────────────────────────────────────────" + echo -e "${BLACK_ON_WHITE} Auto Sort — Analysephase ${NC}" + echo "──────────────────────────────────────────────────────────────" - echo "────────────────────────────────────────────────────────────────" + # 1️⃣ Alle Dateien analysieren und Kombinationen erfassen + found_any=false - # Datei relativ zu input_folder + # Sortierordner-Pfad in ein sauberes Pattern für find umwandeln + escaped_output_dir=$(printf '%s\n' "$output_dir" | sed 's/[][\.*^$(){}?+|/]/\\&/g') + + while IFS= read -r file; do + found_any=true relative_path=$(realpath --relative-to="$input_folder" "$file") - # Überspringe Dateien, die "-sample" im Dateinamen haben - if [[ "$file" == *"sample."* ]]; then - - echo -e "${YELLOW_ON_WHITE} Überspringe ${NC} ${YELLOW}$relative_path${NC}" - echo -e "${YELLOW_ON_WHITE} Grund ${NC} ${YELLOW}'sample.' im Dateinamen${NC}" + # Sample-Dateien erkennen (case-insensitive, am Ende des Dateinamens) + basename_lower=$(basename "$file" | tr '[:upper:]' '[:lower:]') + if [[ "$basename_lower" =~ \.sample\.(mkv|mp4|avi|vob|ts|mpeg|mov)$ ]]; then + echo -e "${YELLOW_ON_WHITE} Überspringe Sample ${NC} $relative_path" + echo -e "${YELLOW_ON_WHITE} Grund ${NC} Enthält '.sample' vor Dateiendung${NC}" continue fi - # Extrahiere die Audio-Spuren, Sprache und Codec audio_streams=$(extract_audio_languages_and_codecs "$file") - - # Extrahiere die Untertitel-Spuren, Sprache und "forced"-Tag subtitle_streams=$(extract_subtitle_languages_and_forced_tags "$file") - # Wenn keine Audio- oder Untertitel-Spuren vorhanden sind, überspringe die Datei if [ -z "$audio_streams" ] && [ -z "$subtitle_streams" ]; then - echo -e "${WHITE_ON_RED} Überspringe ${NC} ${RED}$relative_path${NC}" - echo -e "${WHITE_ON_RED} Grund ${NC} ${RED}Keine Audio- oder Untertitel-Spuren gefunden${NC}" + echo -e "${WHITE_ON_RED} Keine Audio-/Subtitle-Spuren: ${NC} $relative_path" continue fi - # Kombiniere die Audio- und Untertitel-Streams mit einem | nur, wenn subtitle_streams nicht leer ist - if [ -z "$subtitle_streams" ]; then - combined_streams="$audio_streams" + # Ausgabe der gefundenen Spuren + echo -e "${WHITE_ON_GRAY} Datei ${NC} $relative_path" + if [ -n "$audio_streams" ]; then + echo -e " 🎧 ${GREEN}Audio:${NC} $audio_streams" else - combined_streams="${audio_streams} | ${subtitle_streams}" + echo -e " 🎧 ${RED}Audio:${NC} (keine)" fi - # Generiere den Ordnernamen basierend auf den kombinierten Streams - folder_name=$(echo "$combined_streams" | tr ' ' ' ') + if [ -n "$subtitle_streams" ]; then + echo -e " 💬 ${YELLOW}Subtitles:${NC} $subtitle_streams" + else + echo -e " 💬 ${RED}Subtitles:${NC} (keine)" + fi - # Zielordner für diese Kombination erstellen + combined_streams="$audio_streams | $subtitle_streams" + files_by_group["$combined_streams"]+="$file"$'\n' + + 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 + ) + + if [ "$found_any" = false ]; then + echo "──────────────────────────────────────────────────────────────" + echo -e "${YELLOW_ON_WHITE} Keine neuen Videodateien gefunden. ${NC}" + echo "──────────────────────────────────────────────────────────────" + read -rp "Zum Schließen bitte [ENTER] drücken ..." + exit 0 + fi + + + + + + # 2️⃣ Ordnernamen prüfen + group_counter=1 + for combo in "${!files_by_group[@]}"; do + folder_name=$(echo "$combo" | tr -s ' ' '_') + target_folder="$output_dir/$folder_name" + if [ ${#target_folder} -gt 240 ]; then + folder_name="Gruppe_${group_counter}" + ((group_counter++)) + fi + group_map["$combo"]="$folder_name" + combinations+=("$combo") + done + + echo "──────────────────────────────────────────────────────────────" + echo -e "${BLACK_ON_WHITE} Verschiebe Dateien ${NC}" + echo "──────────────────────────────────────────────────────────────" + + # 3️⃣ Dateien verschieben + for combo in "${combinations[@]}"; do + folder_name="${group_map[$combo]}" target_folder="$output_dir/$folder_name" mkdir -p "$target_folder" - - echo -e "${WHITE_ON_GRAY} Verschiebe ${NC} $relative_path" - echo -e "${WHITE_ON_GRAY} Nach ${NC}${GREEN}$folder_name${NC}" - - # Datei in den entsprechenden Ordner verschieben - mv "$file" "$target_folder/" - + while IFS= read -r file; do + [ -z "$file" ] && continue + relative_path=$(realpath --relative-to="$input_folder" "$file") + echo -e "${WHITE_ON_GRAY} → ${NC} ${relative_path}" + echo -e " nach ${GREEN}$folder_name${NC}" + mv "$file" "$target_folder/" + done <<< "${files_by_group[$combo]}" done + echo "──────────────────────────────────────────────────────────────" + echo -e "${BLACK_ON_WHITE} Fertig! ${NC}" } - - - # Main main - +echo +read -rp "Zum Schließen bitte [ENTER] drücken ..." +exit 0 $SHELL \ No newline at end of file