185
homelab/scripts/reorganize-games-common.sh
Normal file
185
homelab/scripts/reorganize-games-common.sh
Normal file
@@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Упорядочивание папок игр в common/ (HDD 4 TB): GameName/GameName/ (файлы) + appmanifest в GameName/,
|
||||
# затем перенос всех папок GameName из common/ на уровень выше (HDD 4 TB). Итог: HDD 4 TB/GameName/GameName/, HDD 4 TB/GameName/appmanifest_*.acf, common/ пустая.
|
||||
# AppId берётся по имени папки из steam_library_with_sizes.json (резолвер resolve_steam_appid.py).
|
||||
# Ручные сопоставления (CM3, Darksiders 3, DOOMEternal и др.) — в resolve_steam_appid.py MANUAL_MAP.
|
||||
#
|
||||
# Запуск в CT 101. Корневая директория: /mnt/nextcloud-extra/common/ (PARENT = /mnt/nextcloud-extra = HDD 4 TB).
|
||||
#
|
||||
# Подготовка: скопировать в контейнер скрипты и JSON (в одну папку, например /opt/scripts/):
|
||||
# pct push 101 homelab/scripts/reorganize-games-common.sh /opt/scripts/
|
||||
# pct push 101 homelab/scripts/resolve_steam_appid.py /opt/scripts/
|
||||
# pct push 101 homelab/scripts/steam_library_with_sizes.json /opt/scripts/
|
||||
#
|
||||
# Dry-run (ничего не меняет, только вывод; в конце — папки без совпадений):
|
||||
# ssh root@192.168.1.150 'pct exec 101 -- bash /opt/scripts/reorganize-games-common.sh /mnt/nextcloud-extra/common 1 /opt/scripts/resolve_steam_appid.py /opt/scripts/steam_library_with_sizes.json'
|
||||
#
|
||||
# Реальное выполнение (второй аргумент 0):
|
||||
# ssh root@192.168.1.150 'pct exec 101 -- bash /opt/scripts/reorganize-games-common.sh /mnt/nextcloud-extra/common 0 /opt/scripts/resolve_steam_appid.py /opt/scripts/steam_library_with_sizes.json'
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="${1:?Usage: $0 <ROOT_DIR> [DRY_RUN=1] [RESOLVER_SCRIPT] [LIBRARY_JSON]}"
|
||||
DRY_RUN="${2:-1}"
|
||||
RESOLVER_SCRIPT="${3:-}"
|
||||
LIBRARY_JSON="${4:-}"
|
||||
[[ -z "$RESOLVER_SCRIPT" ]] && [[ -f ./resolve_steam_appid.py ]] && RESOLVER_SCRIPT="./resolve_steam_appid.py"
|
||||
[[ -n "$RESOLVER_SCRIPT" ]] && [[ -z "$LIBRARY_JSON" ]] && LIBRARY_JSON="$(dirname "$RESOLVER_SCRIPT")/steam_library_with_sizes.json"
|
||||
[[ -z "$RESOLVER_SCRIPT" ]] && {
|
||||
echo "ERROR: resolve_steam_appid.py not found. Pass path as 3rd arg or run from script dir."
|
||||
exit 1
|
||||
}
|
||||
[[ ! -f "$RESOLVER_SCRIPT" ]] && {
|
||||
echo "ERROR: resolver script not found: $RESOLVER_SCRIPT"
|
||||
exit 1
|
||||
}
|
||||
[[ -z "$LIBRARY_JSON" ]] || [[ ! -f "$LIBRARY_JSON" ]] && {
|
||||
echo "ERROR: steam_library_with_sizes.json not found. Pass path as 4th arg or put next to resolver."
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo "=== ROOT: $ROOT | DRY_RUN: $DRY_RUN | Resolver: $RESOLVER_SCRIPT | Library: $LIBRARY_JSON ==="
|
||||
echo ""
|
||||
|
||||
if [[ ! -d "$ROOT" ]]; then
|
||||
echo "ERROR: not a directory: $ROOT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PARENT_DIR="$(dirname "$ROOT")"
|
||||
echo "Папки после обработки будут перенесены: $ROOT -> $PARENT_DIR (уровень выше)"
|
||||
echo ""
|
||||
|
||||
MISSING_APPID=()
|
||||
MISSING_MANIFEST=()
|
||||
RESOLVED=()
|
||||
PROCESSED_DIRS=()
|
||||
|
||||
while IFS= read -r -d '' dir; do
|
||||
base=$(basename "$dir")
|
||||
[[ "$base" == "lost+found" ]] && continue
|
||||
|
||||
appid=""
|
||||
appid=$(python3 "$RESOLVER_SCRIPT" "$LIBRARY_JSON" "$base" 2>/dev/null || true)
|
||||
if [[ -z "$appid" ]]; then
|
||||
MISSING_APPID+=("$base")
|
||||
echo "[$base] appId не найден по имени — пропуск"
|
||||
echo ""
|
||||
continue
|
||||
fi
|
||||
|
||||
manifest="$ROOT/appmanifest_${appid}.acf"
|
||||
# Манифест может лежать в корне common/ или на уровень выше (steamapps/)
|
||||
if [[ ! -f "$manifest" ]]; then
|
||||
manifest_parent="$(dirname "$ROOT")/appmanifest_${appid}.acf"
|
||||
if [[ -f "$manifest_parent" ]]; then
|
||||
manifest="$manifest_parent"
|
||||
else
|
||||
MISSING_MANIFEST+=("$base -> appmanifest_${appid}.acf")
|
||||
fi
|
||||
fi
|
||||
|
||||
inner="$dir/$base"
|
||||
if [[ -d "$inner" ]]; then
|
||||
echo "[$base] (уже есть вложенная $base/) appId=$appid"
|
||||
for item in "$dir"/*; do
|
||||
[[ -e "$item" ]] || continue
|
||||
iname=$(basename "$item")
|
||||
[[ "$iname" == "$base" ]] && 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
|
||||
dest_manifest="$dir/appmanifest_${appid}.acf"
|
||||
if [[ "$manifest" != "$dest_manifest" ]]; then
|
||||
if [[ $DRY_RUN -eq 1 ]]; then
|
||||
echo " [DRY-RUN] cp/mv '$manifest' -> '$dest_manifest'"
|
||||
else
|
||||
cp "$manifest" "$dest_manifest" 2>/dev/null || mv "$manifest" "$dest_manifest"
|
||||
echo " appmanifest_${appid}.acf -> $dir/"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "[$base] (создать $base/$base/ и перенести файлы) appId=$appid"
|
||||
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" == 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
|
||||
dest_manifest="$dir/appmanifest_${appid}.acf"
|
||||
if [[ "$manifest" != "$dest_manifest" ]]; then
|
||||
if [[ $DRY_RUN -eq 1 ]]; then
|
||||
echo " [DRY-RUN] cp/mv '$manifest' -> '$dest_manifest'"
|
||||
else
|
||||
cp "$manifest" "$dest_manifest" 2>/dev/null || mv "$manifest" "$dest_manifest"
|
||||
echo " appmanifest_${appid}.acf -> $dir/"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
RESOLVED+=("$base -> $appid")
|
||||
PROCESSED_DIRS+=("$base")
|
||||
echo ""
|
||||
done < <(find "$ROOT" -maxdepth 1 -type d ! -path "$ROOT" -print0 | sort -z)
|
||||
|
||||
echo "=== Перенос папок из common/ на уровень выше (HDD 4 TB) ==="
|
||||
for base in "${PROCESSED_DIRS[@]}"; do
|
||||
src="$ROOT/$base"
|
||||
dst="$PARENT_DIR/$base"
|
||||
if [[ $DRY_RUN -eq 1 ]]; then
|
||||
echo " [DRY-RUN] mv '$src' -> '$PARENT_DIR/'"
|
||||
else
|
||||
if mv "$src" "$PARENT_DIR/" 2>/dev/null; then
|
||||
echo " mv $base -> $PARENT_DIR/"
|
||||
else
|
||||
# Цель уже существует (например, дубликат игры) — сливаем содержимое и удаляем источник
|
||||
if [[ -d "$dst" ]]; then
|
||||
inner="$src/$base"
|
||||
if [[ -d "$inner" ]]; then
|
||||
mkdir -p "$dst/$base"
|
||||
for item in "$inner"/*; do [[ -e "$item" ]] && mv "$item" "$dst/$base/"; done
|
||||
rmdir "$inner" 2>/dev/null || true
|
||||
fi
|
||||
for f in "$src"/appmanifest_*.acf; do [[ -f "$f" ]] && cp -n "$f" "$dst/" 2>/dev/null || true; done
|
||||
rm -rf "$src"
|
||||
echo " merge $base -> $PARENT_DIR/ (target existed)"
|
||||
else
|
||||
echo " ERROR: could not move $base"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
|
||||
echo "=== Папки, для которых не найден appId ==="
|
||||
for x in "${MISSING_APPID[@]}"; do echo " $x"; done
|
||||
echo ""
|
||||
echo "=== Итог: игры в $PARENT_DIR/GameName/GameName/, манифесты в $PARENT_DIR/GameName/, common/ пустая ==="
|
||||
echo ""
|
||||
echo "=== Нет файла appmanifest (игра найдена по имени) ==="
|
||||
for x in "${MISSING_MANIFEST[@]}"; do echo " $x"; done
|
||||
echo ""
|
||||
echo "=== Сопоставления имя -> appId ==="
|
||||
for x in "${RESOLVED[@]}"; do echo " $x"; done
|
||||
echo ""
|
||||
echo "=== Конец (DRY_RUN=$DRY_RUN) ==="
|
||||
Reference in New Issue
Block a user