#!/usr/bin/env bash # # Упорядочивание папок игр: GameName/GameName/ (файлы) + appmanifest в GameName/ # Запуск в CT 101. Поддерживает две корневые директории: Игры и nextcloud-extra. # # Использование (с хоста Proxmox 192.168.1.150): # # Dry-run (только вывод действий, ничего не меняет): # ssh root@192.168.1.150 'pct exec 101 -- bash -s "/mnt/nextcloud-data/html/data/kerrad/files/Игры" 1' < homelab/scripts/reorganize-games.sh # ssh root@192.168.1.150 'pct exec 101 -- bash -s /mnt/nextcloud-extra 1' < homelab/scripts/reorganize-games.sh # # Реальное выполнение (второй аргумент 0): # ssh root@192.168.1.150 'pct exec 101 -- bash -s "/mnt/nextcloud-data/html/data/kerrad/files/Игры" 0' < homelab/scripts/reorganize-games.sh # ssh root@192.168.1.150 'pct exec 101 -- bash -s /mnt/nextcloud-extra 0' < homelab/scripts/reorganize-games.sh # # Либо скопировать скрипт в контейнер и запустить там: # pct exec 101 -- bash /path/to/reorganize-games.sh "/mnt/.../Игры" 1 # set -euo pipefail ROOT="${1:?Usage: $0 [DRY_RUN=1]}" DRY_RUN="${2:-1}" README="" [ -f "$ROOT/readme.md" ] && README="$ROOT/readme.md" [ -f "$ROOT/Readme.md" ] && README="${README:-$ROOT/Readme.md}" [ -z "$README" ] && { echo "ERROR: no readme.md or Readme.md in $ROOT"; exit 1; } # Маппинг: "имя в readme" -> "имя папки на диске" (если отличается) declare -A NAME_TO_FOLDER # Игры (kerrad/files/Игры) NAME_TO_FOLDER["Mortal Kombat X"]="MK10" NAME_TO_FOLDER["Ведьмак 3"]="The Witcher 3" NAME_TO_FOLDER["Crusader Kings 3"]="Crusader Kings III" NAME_TO_FOLDER["Cities: Skylines"]="Cities_Skylines" NAME_TO_FOLDER["Baldur's gate 3"]="Baldurs Gate 3" NAME_TO_FOLDER["Anno 1800 (full dlc)"]="Anno 1800" NAME_TO_FOLDER["Titan Quest (full dlc)"]="Titan Quest Anniversary Edition" NAME_TO_FOLDER["KCD 2"]="KingdomComeDeliverance2" NAME_TO_FOLDER["FC 24"]="EA Sports FC 24" NAME_TO_FOLDER["Katana Zero"]="Katana ZERO" NAME_TO_FOLDER["L4D2"]="Left 4 Dead 2" NAME_TO_FOLDER["XCOM2"]="XCOM 2" NAME_TO_FOLDER["L.A. Noire"]="L.A.Noire" NAME_TO_FOLDER["ETS 2"]="Euro Truck Simulator 2" NAME_TO_FOLDER["Phantome Doctrine"]="PhantomDoctrine" NAME_TO_FOLDER["Черепашки ниндзя: В поисках Сплинтера"]="Teenage Mutant Ninja Turtles Splintered Fate" NAME_TO_FOLDER["Казаки 3"]="Cossacks 3" NAME_TO_FOLDER["A Way Out"]="AWayOut" NAME_TO_FOLDER["Counter Strike 2"]="Counter-Strike Global Offensive" NAME_TO_FOLDER["Sid Meier's Civilization IV"]="Sid Meier's Civilization VI" # nextcloud-extra (readme: "Assasins" с одной s, на диске часто "Assassin's") NAME_TO_FOLDER["Assasins Creed"]="Assassins Creed" NAME_TO_FOLDER["Bully"]="Bully Scholarship Edition" NAME_TO_FOLDER["Assasins Creed 2"]="Assassin's Creed 2" NAME_TO_FOLDER["Assasins Creed III Remastered"]="Assassin's Creed III Remastered" NAME_TO_FOLDER["Assasins Creed IV Black Flag"]="Assassin's Creed IV Black Flag" NAME_TO_FOLDER["Assasins Creed Revelations"]="Assassin's Creed Revelations" NAME_TO_FOLDER["Assasins Creed Syndicate"]="Assassin's Creed Syndicate" NAME_TO_FOLDER["Assasins Creed Unity"]="Assassin's Creed Unity" NAME_TO_FOLDER["Assasins Creed Valhalla"]="Assassin's Creed Valhalla" NAME_TO_FOLDER["Assasins Creed Mirage"]="Assassin's Creed Mirage" NAME_TO_FOLDER["Assassins Creed Brotherhood"]="Assassins Creed Brotherhood" NAME_TO_FOLDER["Assassins Creed Odyssey"]="Assassins Creed Odyssey" NAME_TO_FOLDER["Assassins Creed Origins"]="Assassins Creed Origins" NAME_TO_FOLDER["Bioshock Remastered"]="BioShock Remastered" NAME_TO_FOLDER["Bioshock Infinite"]="BioShock Infinite" NAME_TO_FOLDER["Bioshock 2 Remastered"]="BioShock 2 Remastered" normalize() { echo "$1" | tr '[:upper:]' '[:lower:]' | tr -s ' \t' ' ' | sed "s/[[:punct:]]//g" | sed 's/^ *//;s/ *$//' } # Парсим readme: строки таблицы | ... | название | appid | declare -A README_NAME_TO_APPID while IFS= read -r line; do [[ "$line" != "|"* ]] && continue # Убираем первый и последний |, разбиваем по | rest="${line#|}" rest="${rest%|}" rest="${rest# }" rest="${rest% }" # Колонки: жанр | название | appmanifest name="" appid="" col=0 while [[ -n "$rest" ]]; do if [[ "$rest" == *"|"* ]]; then part="${rest%%|*}" rest="${rest#*|}" else part="$rest" rest="" fi part=$(echo "$part" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') if [[ $col -eq 1 ]]; then name="$part" elif [[ $col -eq 2 ]]; then appid="${part// /}" fi ((col++)) || true done [[ -z "$name" || -z "$appid" ]] && continue [[ "$name" == "название игры" ]] && continue [[ "$name" == "appmanifest" ]] && continue README_NAME_TO_APPID["$name"]="$appid" done < "$README" # По имени из readme находим папку на диске resolve_folder() { local readme_name="$1" if [[ -n "${NAME_TO_FOLDER[$readme_name]:-}" ]]; then echo "${NAME_TO_FOLDER[$readme_name]}" return fi local norm=$(normalize "$readme_name") for dir in "$ROOT"/*/; do [[ -d "$dir" ]] || continue base=$(basename "$dir") [[ "$base" == "моды" || "$base" == "Моды" || "$base" == "lost+found" ]] && continue if [[ $(normalize "$base") == "$norm" ]]; then echo "$base" return fi done echo "" } # По имени папки находим appid из readme (перебор по всем именам в readme) resolve_appid() { local folder_name="$1" local norm_folder=$(normalize "$folder_name") for readme_name in "${!README_NAME_TO_APPID[@]}"; do local f=$(resolve_folder "$readme_name") if [[ "$f" == "$folder_name" ]]; then echo "${README_NAME_TO_APPID[$readme_name]}" return fi done # Прямое совпадение по нормализованному имени папки for readme_name in "${!README_NAME_TO_APPID[@]}"; do if [[ $(normalize "$readme_name") == "$norm_folder" ]]; then echo "${README_NAME_TO_APPID[$readme_name]}" return fi done echo "" } echo "=== ROOT: $ROOT | DRY_RUN: $DRY_RUN ===" echo "" # Собираем папки игр (директории, не файлы) MISSING_APPID=() MISSING_MANIFEST=() while IFS= read -r -d '' dir; do base=$(basename "$dir") [[ "$base" == "моды" || "$base" == "Моды" || "$base" == "lost+found" ]] && continue appid=$(resolve_appid "$base") if [[ -z "$appid" ]]; then MISSING_APPID+=("$base (нет в readme)") continue fi manifest="$ROOT/appmanifest_${appid}.acf" if [[ ! -f "$manifest" ]]; then MISSING_MANIFEST+=("$base -> appmanifest_${appid}.acf") fi inner="$dir/$base" if [[ -d "$inner" ]]; then # Уже есть вложенная папка с тем же именем echo "[$base] (есть вложенная $base/)" for item in "$dir"/*; do [[ -e "$item" ]] || continue iname=$(basename "$item") [[ "$iname" == "$base" ]] && continue [[ "$iname" == "readme.md" || "$iname" == "Readme.md" ]] && continue [[ "$iname" == appmanifest_*.acf ]] && continue if [[ $DRY_RUN -eq 1 ]]; then echo " [DRY-RUN] mv '$item' -> '$inner/'" else echo " mv '$item' -> '$inner/'" mv "$item" "$inner/" fi done if [[ -f "$manifest" ]]; then if [[ $DRY_RUN -eq 1 ]]; then echo " [DRY-RUN] mv '$manifest' -> '$dir/'" else mv "$manifest" "$dir/" echo " mv appmanifest_${appid}.acf -> $dir/" fi fi else # Нет вложенной папки — создаём GameName/GameName/ и переносим всё echo "[$base] (создать $base/$base/ и перенести файлы)" if [[ $DRY_RUN -eq 1 ]]; then echo " [DRY-RUN] mkdir -p '$inner'" else mkdir -p "$inner" fi for item in "$dir"/*; do [[ -e "$item" ]] || continue iname=$(basename "$item") [[ "$iname" == "$base" ]] && continue [[ "$iname" == "readme.md" || "$iname" == "Readme.md" ]] && continue [[ "$iname" == appmanifest_*.acf ]] && continue if [[ $DRY_RUN -eq 1 ]]; then echo " [DRY-RUN] mv '$item' -> '$inner/'" else mv "$item" "$inner/" echo " mv $iname -> $base/" fi done if [[ -f "$manifest" ]]; then if [[ $DRY_RUN -eq 1 ]]; then echo " [DRY-RUN] mv '$manifest' -> '$dir/'" else mv "$manifest" "$dir/" echo " mv appmanifest_${appid}.acf -> $dir/" fi fi fi echo "" done < <(find "$ROOT" -maxdepth 1 -type d ! -path "$ROOT" -print0 | sort -z) echo "=== Папки без соответствия в readme ===" for x in "${MISSING_APPID[@]}"; do echo " $x"; done echo "" echo "=== Не найдены appmanifest (игра есть в readme) ===" for x in "${MISSING_MANIFEST[@]}"; do echo " $x"; done echo "" echo "=== Конец (DRY_RUN=$DRY_RUN) ==="