233 lines
9.0 KiB
Bash
233 lines
9.0 KiB
Bash
#!/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 <ROOT_DIR> [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) ==="
|