一键脚本
bash <(curl -sL gitee.com/meimolihan/cmdbox/raw/master/sh/tv_rename.sh) '/vol2/1000/media/电视剧集/国产剧/沙尘暴 (2025)/Season 1'
bash <(curl -sL ../sh/tv_rename.sh) '/vol2/1000/media/电视剧集/国产剧/沙尘暴 (2025)/Season 1'
| 模式 | 命令 | 说明 |
|---|---|---|
| 交互模式 | ./脚本名.sh |
交互式选择电视剧目录 |
| 传参模式 | ./脚本名.sh 电视剧根目录 |
指定目录并确认 |
效果预览
补充说明
该脚本用于电视剧视频智能识别重命名,可自动识别剧集编号并规范命名为 前缀-S季E集.扩展名 格式,适配媒体库/刮削工具使用。
功能特点
- 支持交互式菜单操作,流程清晰易用
- 自动扫描目录下视频文件,支持 mp4、mkv、avi、mov、wmv、flv、webm、m4v、mpg、mpeg、kvm 等格式
- 智能识别剧集编号:支持 SxE 格式、EP 格式、中文第X集、数字格式、开头数字、结尾数字
- 自动提取剧集前缀候选,统计出现次数与占比,支持序号选择、推荐默认、手动输入
- 自动检测季号分布,手动设置季号并校验输入范围
- 支持设置起始集数,自动显示识别到的集数范围与建议值
- 预览重命名结果,显示原文件名、识别类型、新文件名,并统计识别准确率
- 重命名前确认,执行后显示成功与失败统计
- 智能模式分析:统计各剧集编号模式的文件占比并给出示例
- 自动检测重命名方案:检测季号、中文剧名、英文剧名及置信度
- 彩色输出:使用 ANSI 颜色码区分信息、成功、警告、错误等提示
输出说明
脚本输出包含以下字段:
| 字段 | 说明 |
|---|---|
| 当前设置 | 显示已设置的剧集前缀、季号、起始集数 |
| 文件列表 | 显示识别到的视频文件及识别到的集数、编号模式 |
| 模式分布 | 显示各识别模式的文件数量与占比 |
| 剧集前缀 | 显示从文件名中提取的剧名前缀候选及出现次数 |
| 季号检测 | 显示从文件名中检测到的季号分布 |
| 集数范围 | 显示识别到的最小、最大集数与建议起始集数 |
| 重命名预览 | 显示原文件名、识别类型与新文件名对比 |
| 识别准确率 | 显示预览完成后高置信度模式的占比 |
| 执行结果 | 显示成功与失败的重命名文件数量 |
| 状态提示 | [信息]、[成功]、[警告]、[错误] 等彩色提示信息 |
注意事项
- 脚本仅对当前目录下的文件进行操作,不递归处理子目录
- 重命名操作不可逆,建议先预览确认后再执行
- 智能识别功能基于文件名模式,可能不准确,建议手动检查
- 重命名规则为:
剧集前缀-S季号E集数.扩展名,自动保留原扩展名 - 设置前缀后需先预览重命名结果,方可执行重命名
- 起始集数范围为 1-999,季号范围为 1-99
- 数字年份(1000-2100)会被识别引擎过滤,避免误识别为集数
- 脚本使用 bash 编写,确保运行环境支持 bash
- 重命名操作需要对应文件的写权限
- 智能识别功能主要针对常见剧集命名格式,非常规格式可能需要手动调整
脚本源码
#!/bin/bash
set -uo pipefail
gl_hui='\033[38;5;59m'
gl_hong='\033[38;5;9m'
gl_lv='\033[38;5;10m'
gl_huang='\033[38;5;11m'
gl_lan='\033[38;5;32m'
gl_bai='\033[38;5;15m'
gl_zi='\033[38;5;13m'
gl_bufan='\033[38;5;14m'
TARGET_DIR="${1:-.}"
if ! TARGET_DIR=$(realpath -- "$TARGET_DIR" 2>/dev/null); then
echo -e "${gl_hong}[错误]${gl_bai} 路径解析失败,请检查路径格式${gl_bai}"
exit 1
fi
if [[ ! -d "$TARGET_DIR" ]]; then
echo -e "${gl_hong}[错误]${gl_bai} 目录不存在:$TARGET_DIR${gl_bai}"
exit 1
fi
if ! cd -- "$TARGET_DIR" 2>/dev/null; then
echo -e "${gl_hong}[错误]${gl_bai} 无法进入目录,权限不足:$TARGET_DIR${gl_bai}"
exit 1
fi
log_info() { echo -e "${gl_lan}[信息]${gl_bai} $*${gl_bai}"; }
log_ok() { echo -e "${gl_lv}[成功]${gl_bai} $*${gl_bai}"; }
log_warn() { echo -e "${gl_huang}[警告]${gl_bai} $*${gl_bai}"; }
log_error() { echo -e "${gl_hong}[错误]${gl_bai} $*${gl_bai}" >&2; }
break_end() {
echo -e "${gl_lv}操作完成${gl_bai}"
echo -e "${gl_bai}按任意键继续 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}\c${gl_bai}"
read -r -n 1 -s -p ""
echo ""
clear
}
sleep_fractional() {
local seconds=$1
if sleep "$seconds" 2>/dev/null; then return 0; fi
if command -v perl >/dev/null 2>&1; then perl -e "select(undef, undef, undef, $seconds)"; return 0; fi
if command -v python3 >/dev/null 2>&1; then python3 -c "import time; time.sleep($seconds)"; return 0; fi
if command -v python >/dev/null 2>&1; then python -c "import time; time.sleep($seconds)"; return 0; fi
local int_seconds=$(echo "$seconds" | awk '{print int($1+0.999)}')
sleep "$int_seconds"
}
cancel_return() {
local menu_name="${1:-上一级选单}"
echo -ne "${gl_lv}即将返回 ${gl_huang}${menu_name} ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}\c${gl_bai}"
sleep_fractional 0.5
echo -ne "${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}\c${gl_bai}"
sleep_fractional 0.6
echo ""
clear
}
handle_y_n() {
echo -ne "\r${gl_hong}无效的选择,请输入 ${gl_bai}(${gl_lv}y${gl_bai}或${gl_hong}N${gl_bai}) ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}\c${gl_bai}"
sleep_fractional 0.3
echo -ne "\r${gl_huang}无效的选择,请输入 ${gl_bai}(${gl_lv}y${gl_bai}或${gl_hong}N${gl_bai}) ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}\c${gl_bai}"
sleep_fractional 0.3
echo -ne "\r${gl_lv}无效的选择,请输入 ${gl_bai}(${gl_lv}y${gl_bai}或${gl_hong}N${gl_bai}) ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}\c${gl_bai}"
sleep_fractional 0.6
echo ""
return 2
}
exit_animation() {
echo -ne "\r${gl_lv}即将退出 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}\c${gl_bai}"
sleep_fractional 1
echo -ne "${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}\c${gl_bai}"
sleep_fractional 1
echo ""
clear
}
exit_script() {
local frames=("⠋" "⠙" "⠹" "⠸" "⠼" "⠴" "⠦" "⠧" "⠇" "⠏")
local dots=(
"${gl_hong}."
"${gl_huang}."
"${gl_lv}."
"${gl_bufan}."
"${gl_zi}."
)
local dot_buffer=""
local frame_len=${#frames[@]}
local dot_idx=0
local total_dots=${#dots[@]}
for ((i=0; i<20; i++)); do
if (( i > 0 && i % 3 == 0 && dot_idx < total_dots )); then
dot_buffer+=${dots[$dot_idx]}
((dot_idx++))
fi
echo -ne "\r\033[K${gl_bufan}${frames[i % frame_len]}${gl_bai} 正在退出 ${dot_buffer}"
sleep_fractional 0.06
done
echo -e "\r\033[K${gl_lv}✓${gl_bai} 成功退出\n"
clear
exit 0
}
handle_invalid_input() {
echo -ne "\r\033[K${gl_huang}无效的输入,请重新输入! ${gl_zi} 1 ${gl_huang} 秒后返回${gl_bai}"
sleep_fractional 1
echo -ne "\r\033[K${gl_lv}无效的输入,请重新输入! ${gl_zi}0${gl_lv} 秒后返回${gl_bai}"
sleep_fractional 0.5
echo -ne "\r\033[K"
continue
}
rename_tv_files_ultimate() {
local VIDEO_EXTS=("mp4" "mkv" "avi" "mov" "wmv" "flv" "webm" "m4v" "mpg" "mpeg" "kvm")
local PREFIX=""
local SEASON="01"
local START_EP="1"
local preview_mode=1
local rename_count=0
local auto_detected=0
local detection_results=()
local prefix_set=0
local preview_done=0
safe_printf() {
local format="$1"
local number="$2"
if [[ "$format" == "%d" ]] || [[ "$format" == "%02d" ]] || [[ "$format" == "%03d" ]]; then
number=$(echo "$number" | sed 's/^0*//')
if [ -z "$number" ]; then
number=0
fi
fi
printf "$format" "$number"
}
enhanced_extract_episode_info() {
local filename="$1"
local results=()
if [[ "$filename" =~ [Ss]([0-9]{1,2})[Ee]([0-9]{1,3}) ]]; then
local season="${BASH_REMATCH[1]}"
local episode="${BASH_REMATCH[2]}"
results+=("S${season}E${episode}:${season}:${episode}:SxE格式")
fi
if [[ "$filename" =~ [Ee][Pp]([0-9]{1,3})[^0-9] ]]; then
local episode="${BASH_REMATCH[1]}"
results+=("EP${episode}:01:${episode}:EP格式")
fi
if [[ "$filename" =~ 第([0-9]{1,3})[集話话] ]]; then
local episode="${BASH_REMATCH[1]}"
results+=("第${episode}集:01:${episode}:中文第X集")
fi
if [[ "$filename" =~ [^0-9]([0-9]{1,3})[^0-9] ]]; then
local num="${BASH_REMATCH[1]}"
if [ "$num" -lt 1000 ] || [ "$num" -gt 2100 ]; then
results+=("${num}:01:${num}:数字格式")
fi
fi
if [[ "$filename" =~ ^([0-9]{1,3}) ]]; then
local episode="${BASH_REMATCH[1]}"
results+=("${episode}:01:${episode}:开头数字")
fi
if [[ "$filename" =~ ([0-9]{1,3})\.[^.]*$ ]]; then
local episode="${BASH_REMATCH[1]}"
results+=("${episode}:01:${episode}:结尾数字")
fi
if [ ${#results[@]} -eq 0 ]; then
results+=("0:01:1:未识别")
fi
printf "%s\n" "${results[@]}"
}
analyze_filename_patterns() {
local files=("$@")
echo -e "${gl_zi}>>> 智能模式分析${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
local pattern_array=()
local example_array=()
local count_array=()
for file in "${files[@]}"; do
local filename=$(basename -- "$file")
local results=($(enhanced_extract_episode_info "$filename"))
for result in "${results[@]}"; do
IFS=':' read -r pattern season episode type <<<"$result"
if [ "$type" != "未识别" ]; then
local found=0
for i in "${!pattern_array[@]}"; do
if [ "${pattern_array[i]}" = "$type" ]; then
count_array[i]=$((count_array[i] + 1))
found=1
break
fi
done
if [ $found -eq 0 ]; then
pattern_array+=("$type")
example_array+=("$filename")
count_array+=(1)
fi
break
fi
done
done
if [ ${#pattern_array[@]} -eq 0 ]; then
echo -e "${gl_huang}未检测到有效的剧集编号模式${gl_bai}"
else
echo -e "${gl_bai}检测到的剧集编号模式:${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
for ((i = 0; i < ${#pattern_array[@]}; i++)); do
local type="${pattern_array[i]}"
local count="${count_array[i]}"
local example="${example_array[i]}"
local percentage=$((count * 100 / ${#files[@]}))
local index=$((i + 1))
if [ $percentage -ge 50 ]; then
echo -e " ${gl_lv}${index}.${gl_bai} ${gl_bufan}${type}${gl_bai} (${percentage}% 文件)${gl_bai}"
else
echo -e " ${gl_huang}${index}.${gl_bai} ${gl_bufan}${type}${gl_bai} (${percentage}% 文件)${gl_bai}"
fi
echo -e " 示例: ${gl_hui}${example}${gl_bai}"
done
fi
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
return 0
}
auto_detect_rename_plan() {
local files=("$@")
local plans=()
clear
echo -e "${gl_zi}>>> 自动检测重命名方案${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
local season_array=()
local season_count_array=()
for file in "${files[@]}"; do
local filename=$(basename -- "$file")
if [[ "$filename" =~ [Ss]([0-9]{1,2})[Ee] ]]; then
local season="${BASH_REMATCH[1]}"
local found=0
for i in "${!season_array[@]}"; do
if [ "${season_array[i]}" = "$season" ]; then
season_count_array[i]=$((season_count_array[i] + 1))
found=1
break
fi
done
if [ $found -eq 0 ]; then
season_array+=("$season")
season_count_array+=(1)
fi
fi
done
if [ ${#season_array[@]} -gt 0 ]; then
local best_season=""
local best_count=0
for i in "${!season_array[@]}"; do
if [ ${season_count_array[i]} -gt $best_count ]; then
best_count=${season_count_array[i]}
best_season="${season_array[i]}"
fi
done
if [ -n "$best_season" ]; then
local formatted_season=$(safe_printf "%02d" "$best_season")
plans+=("保持原季号|S${formatted_season}|检测到S${formatted_season}格式|高")
fi
fi
if [ ${#files[@]} -gt 0 ]; then
local sample_file=$(basename -- "${files[0]}")
if [[ "$sample_file" =~ ^([^0-9.-[:space:]]+)[^0-9]* ]]; then
local chinese_name="${BASH_REMATCH[1]}"
chinese_name=$(echo "$chinese_name" | sed 's/^[[:space:][:punct:]]*//;s/[[:space:][:punct:]]*$//')
if [ -n "$chinese_name" ] && [ ${#chinese_name} -ge 2 ]; then
plans+=("中文剧名|${chinese_name}|从文件名提取中文名|中")
fi
fi
if [[ "$sample_file" =~ \.([A-Za-z][A-Za-z. ]+?)[^A-Za-z] ]]; then
local english_name="${BASH_REMATCH[1]}"
english_name=$(echo "$english_name" | sed 's/\./ /g;s/ $//')
if [ -n "$english_name" ]; then
plans+=("英文剧名|${english_name}|从文件名提取英文名|中")
fi
fi
fi
if [ ${#plans[@]} -eq 0 ]; then
echo -e "${gl_huang}未检测到有效的重命名方案${gl_bai}"
return 1
fi
echo -e "${gl_bai}检测到的重命名方案:${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
for ((i = 0; i < ${#plans[@]}; i++)); do
IFS='|' read -r type value description confidence <<<"${plans[$i]}"
local index=$((i + 1))
case $confidence in
高) color="${gl_lv}" ;;
中) color="${gl_huang}" ;;
*) color="${gl_hong}" ;;
esac
echo -e " ${gl_bufan}${index}.${gl_bai} ${color}${type}${gl_bai}"
echo -e " 值: ${gl_bufan}${value}${gl_bai}"
echo -e " 说明: ${gl_hui}${description}${gl_bai}"
echo -e " 置信度: ${color}${confidence}${gl_bai}"
echo ""
done
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
return 0
}
scan_video_files() {
local -n files_ref=$1
local -n episode_info_ref=$2
local -n pattern_types_ref=$3
local ext_pattern=""
for ext in "${VIDEO_EXTS[@]}"; do
if [ -z "$ext_pattern" ]; then
ext_pattern="-name \"*.$ext\""
else
ext_pattern="$ext_pattern -o -name \"*.$ext\""
fi
done
while IFS= read -r file; do
if [ -f "$file" ]; then
local filename=$(basename -- "$file")
local extension="${filename##*.}"
local results=($(enhanced_extract_episode_info "$filename"))
local found=0
for result in "${results[@]}"; do
IFS=':' read -r pattern season episode type <<<"$result"
if [ "$episode" != "0" ] && { [ "$episode" != "1" ] || [ "$type" != "未识别" ]; }; then
files_ref+=("$file")
episode_info_ref+=("$episode:$season:$type")
pattern_types_ref+=("$type")
found=1
break
fi
done
if [ $found -eq 0 ]; then
log_warn "无法识别集数: $filename"
fi
fi
done < <(eval "find . -maxdepth 1 -type f \( $ext_pattern \) 2>/dev/null" | sort -V)
}
while true; do
clear
echo -e "${gl_zi}>>> 电视剧文件重命名${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
local files=()
local episode_info=()
local pattern_types=()
scan_video_files files episode_info pattern_types
if [ ${#files[@]} -eq 0 ]; then
log_error "未找到可识别的视频文件!"
echo -e "${gl_bai}支持格式: ${gl_bufan}${VIDEO_EXTS[*]}${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e "${gl_huang}0. ${gl_bai}返回上一级选单${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
read -r -e -p "$(echo -e "${gl_bai}请按任意键继续 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai} ")" -n 1
return
fi
if [ ${#files[@]} -gt 0 ]; then
local sorted_files=()
local sorted_episodes=()
while IFS= read -r line; do
IFS=':' read -r episode season type <<<"$line"
for i in "${!files[@]}"; do
if [[ "${episode_info[$i]}" == "$line" ]]; then
sorted_episodes+=("$episode:$season:$type")
sorted_files+=("${files[$i]}")
break
fi
done
done < <(printf "%s\n" "${episode_info[@]}" | sort -t: -k1,1n)
files=("${sorted_files[@]}")
episode_info=("${sorted_episodes[@]}")
fi
echo -e "${gl_huang}找到 ${gl_lv}${#files[@]} ${gl_huang}个可识别文件:${gl_bai}"
echo -e ""
for ((i = 0; i < ${#files[@]}; i++)); do
local filename=$(basename -- "${files[$i]}")
IFS=':' read -r episode season type <<<"${episode_info[$i]}"
echo -e " ${gl_bufan}$(safe_printf "%02d" $((i + 1))).${gl_bai} E$(safe_printf "%02d" "$episode") [${type}] - $filename${gl_bai}"
done
if [ $auto_detected -eq 0 ] && [ ${#files[@]} -gt 0 ]; then
echo -e ""
echo -e "${gl_bai}检测到的模式分布:${gl_bai}"
local pattern_array=()
local count_array=()
for pattern in "${pattern_types[@]}"; do
local found=0
for i in "${!pattern_array[@]}"; do
if [ "${pattern_array[i]}" = "$pattern" ]; then
count_array[i]=$((count_array[i] + 1))
found=1
break
fi
done
if [ $found -eq 0 ]; then
pattern_array+=("$pattern")
count_array+=(1)
fi
done
for i in "${!pattern_array[@]}"; do
local pattern="${pattern_array[i]}"
local count="${count_array[i]}"
local percentage=$((count * 100 / ${#files[@]}))
echo -e " ${gl_bufan}${pattern}:${gl_lv} ${count} ${gl_bai}文件 (${percentage}%)${gl_bai}"
done
fi
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
if [ -n "$PREFIX" ]; then
echo -e "${gl_bufan}当前设置:${gl_bai}"
echo -e " ${gl_bufan}剧集前缀:${gl_bai} $PREFIX${gl_bai}"
echo -e " ${gl_bufan}起始季号:${gl_bai} S$SEASON${gl_bai}"
echo -e " ${gl_bufan}起始集数:${gl_bai} E$(safe_printf "%02d" $START_EP)${gl_bai}"
if [ $auto_detected -eq 1 ]; then
echo -e " ${gl_lv}✓ 智能检测已应用${gl_bai}"
fi
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
fi
# =========菜单行根据状态切换显示文字颜色=========
if [[ $prefix_set -eq 1 ]]; then
echo -e "${gl_bufan}1. ${gl_bai}设置剧集前缀 ${gl_lv}★ 已设置${gl_bai}"
else
echo -e "${gl_bufan}1. ${gl_bai}设置剧集前缀 ${gl_hong}★ 必须设置${gl_bai}"
fi
echo -e "${gl_bufan}2. ${gl_bai}设置起始季号 ${gl_huang}★ 默认S01(非必选)${gl_bai}"
echo -e "${gl_bufan}3. ${gl_bai}设置起始集数 ${gl_huang}★ 默认E01(非必选)${gl_bai}"
if [[ $preview_done -eq 1 ]]; then
echo -e "${gl_bufan}4. ${gl_bai}预览命名结果 ${gl_lv}★ 已预览${gl_bai}"
else
echo -e "${gl_bufan}4. ${gl_bai}预览命名结果 ${gl_hong}★ 强制预览${gl_bai}"
fi
if [ $preview_mode -eq 1 ] && [ -n "$PREFIX" ]; then
echo -e "${gl_bufan}5. ${gl_lv}执行重命名 ✓ 可执行${gl_bai}"
else
echo -e "${gl_bufan}5. ${gl_bai}执行重命名 ${gl_hong}✗ 不可执行${gl_bai}"
fi
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e "${gl_hong}0. ${gl_bai}退出脚本${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
read -r -e -p "$(echo -e "${gl_bai}请输入你的选择: ")" choice
case $choice in
1)
clear
echo -e "${gl_zi}>>> 设置剧集前缀${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
extract_prefix_candidate() {
local filename="$1"
local basename="${filename%.*}"
local results=($(enhanced_extract_episode_info "$filename"))
if [ ${#results[@]} -gt 0 ]; then
IFS=':' read -r pattern season episode type <<<"${results[0]}"
if [ "$type" != "未识别" ] && [ -n "$pattern" ]; then
local cleaned=$(echo "$basename" | sed -E "s/[._ -]*${pattern}[._ -]*//g")
if [ -n "$cleaned" ]; then
cleaned=$(echo "$cleaned" | sed 's/^[._ -]*//;s/[._ -]*$//')
if [ -n "$cleaned" ] && [ ${#cleaned} -ge 2 ]; then
echo "$cleaned"
return
fi
fi
fi
fi
local fallback=$(echo "$basename" | sed 's/^[._ -]*//;s/[._ -]*$//')
if [ -n "$fallback" ] && [ ${#fallback} -ge 2 ]; then
echo "$fallback"
else
echo ""
fi
}
local candidates=()
local counts=()
local total_files=${#files[@]}
for file in "${files[@]}"; do
local filename=$(basename -- "$file")
local candidate=$(extract_prefix_candidate "$filename")
if [ -z "$candidate" ]; then
continue
fi
local found=-1
for idx in "${!candidates[@]}"; do
if [ "${candidates[$idx]}" = "$candidate" ]; then
found=$idx
break
fi
done
if [ $found -ge 0 ]; then
counts[$found]=$((counts[$found] + 1))
else
candidates+=("$candidate")
counts+=(1)
fi
done
if [ ${#candidates[@]} -eq 0 ]; then
candidates=("电视剧")
counts=($total_files)
log_warn "无法自动识别前缀,使用默认值: 电视剧"
fi
for ((i = 0; i < ${#candidates[@]} - 1; i++)); do
for ((j = i + 1; j < ${#candidates[@]}; j++)); do
if [ ${counts[$j]} -gt ${counts[$i]} ]; then
tmp_c="${candidates[$i]}"
tmp_n="${counts[$i]}"
candidates[$i]="${candidates[$j]}"
counts[$i]="${counts[$j]}"
candidates[$j]="$tmp_c"
counts[$j]="$tmp_n"
fi
done
done
local display_limit=10
if [ ${#candidates[@]} -gt $display_limit ]; then
candidates=("${candidates[@]:0:$display_limit}")
counts=("${counts[@]:0:$display_limit}")
fi
echo -e "${gl_bai}从文件名中提取到的剧名前缀:${gl_bai}"
for i in "${!candidates[@]}"; do
local idx=$((i + 1))
local percentage=$((counts[i] * 100 / total_files))
echo -e " ${gl_bufan}${idx}.${gl_bai} ${gl_lv}${candidates[$i]}${gl_bai} (出现 ${counts[$i]} 次, ${percentage}%)${gl_bai}"
done
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e "${gl_bai}推荐: ${gl_lv}${candidates[0]}${gl_bai} (${gl_huang}回车直接使用推荐${gl_bai})${gl_bai}"
echo -e "${gl_bai}示例: ${gl_bufan}${candidates[0]}-S${SEASON}E01.扩展名${gl_bai}"
echo -e "${gl_bai}指定: ${gl_bai}手动输入自定义前缀${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
read -r -e -p "$(echo -e "${gl_bai}请输入序号或自定义前缀 (${gl_huang}0${gl_bai}返回): ")" input_prefix
[[ "$input_prefix" == "0" ]] && { cancel_return "上一级选单"; continue; }
if [[ "$input_prefix" =~ ^[0-9]+$ ]] && [ "$input_prefix" -ge 1 ] && [ "$input_prefix" -le ${#candidates[@]} ]; then
PREFIX="${candidates[$((input_prefix - 1))]}"
log_ok "剧集前缀已设置为: $PREFIX (通过序号选择)"
elif [ -z "$input_prefix" ]; then
PREFIX="${candidates[0]}"
log_ok "剧集前缀已设置为: $PREFIX (使用推荐)"
else
PREFIX="$input_prefix"
log_ok "剧集前缀已设置为: $PREFIX (手动输入)"
fi
preview_mode=0
prefix_set=1 # 标记前缀已设置
preview_done=0 # 修改前缀后重置预览状态
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
;;
2)
echo -e ""
echo -e "${gl_zi}>>> 设置季号${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
local season_array=()
local season_count_array=()
for info in "${episode_info[@]}"; do
IFS=':' read -r episode season type <<<"$info"
if [ "$season" != "01" ]; then
local found=0
for i in "${!season_array[@]}"; do
if [ "${season_array[i]}" = "$season" ]; then
season_count_array[i]=$((season_count_array[i] + 1))
found=1
break
fi
done
if [ $found -eq 0 ]; then
season_array+=("$season")
season_count_array+=(1)
fi
fi
done
if [ ${#season_array[@]} -gt 0 ]; then
echo -e "${gl_bai}从文件名检测到季号:${gl_bai}"
for i in "${!season_array[@]}"; do
local season="${season_array[i]}"
local count="${season_count_array[i]}"
local percentage=$((count * 100 / ${#files[@]}))
echo -e " ${gl_bufan}S${season}:${gl_bai} ${count} 文件 (${percentage}%)${gl_bai}"
done
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
fi
echo -e "${gl_bai}当前季号: ${gl_bufan}S${SEASON}${gl_bai}"
echo -e "${gl_bai}示例: ${gl_bufan}01${gl_bai} 表示第 ${gl_bufan}1${gl_bai} 季 (将显示为 S01)${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
read -r -e -p "$(echo -e "${gl_bai}请输入季号 (当前: ${gl_lv}S${SEASON}${gl_bai},回车保持,${gl_huang}0${gl_bai}返回): ")" input_season
[[ "$input_season" == "0" ]] && { cancel_return "上一级选单"; continue; }
if [ -z "$input_season" ]; then
echo -e "${gl_bai}保持当前季号: S${SEASON}${gl_bai}"
continue
fi
if [[ "$input_season" =~ ^[0-9]{1,2}$ ]]; then
SEASON=$(safe_printf "%02d" "$input_season")
log_ok "季号已设置为: S$SEASON"
preview_mode=0
preview_done=0 # 修改季号重置预览状态
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
else
log_error "请输入有效的数字 (1-99)!"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
fi
;;
3)
echo -e ""
echo -e "${gl_zi}>>> 设置起始集数${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
if [ ${#episode_info[@]} -gt 0 ]; then
local first_info="${episode_info[0]}"
local last_info="${episode_info[-1]}"
IFS=':' read -r first_ep first_season first_type <<<"$first_info"
IFS=':' read -r last_ep last_season last_type <<<"$last_info"
echo -e "${gl_bai}识别的集数范围:${gl_bai}"
echo -e " ${gl_bufan}最小:${gl_bai} E$(safe_printf "%02d" "$first_ep") (${first_type})${gl_bai}"
echo -e " ${gl_bufan}最大:${gl_bai} E$(safe_printf "%02d" "$last_ep") (${last_type})${gl_bai}"
echo -e " ${gl_bufan}建议:${gl_bai} 从 E$(safe_printf "%02d" "$first_ep") 开始${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
fi
echo -e "${gl_bai}当前起始集数: ${gl_bufan}E$(safe_printf "%02d" "$START_EP")${gl_bai}"
echo -e "${gl_bai}示例: ${gl_bufan}26${gl_bai} 表示从第 ${gl_bufan}26${gl_bai} 集开始编号${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
read -r -e -p "$(echo -e "${gl_bai}请输入起始集数 (当前: ${gl_lv}E$(safe_printf "%02d" "$START_EP")${gl_bai},回车保持,${gl_huang}0${gl_bai}返回): ")" input_start
[[ "$input_start" == "0" ]] && { cancel_return "上一级选单"; continue; }
if [ -z "$input_start" ]; then
echo -e "${gl_bai}保持当前起始集数: E$(safe_printf "%02d" "$START_EP")${gl_bai}"
continue
fi
if [[ "$input_start" =~ ^[0-9]{1,3}$ ]] && [ "$input_start" -ge 1 ]; then
START_EP="$input_start"
log_ok "起始集数已设置为: E$(safe_printf "%02d" "$START_EP")"
preview_mode=0
preview_done=0 # 修改起始集数重置预览状态
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
else
log_error "请输入有效的集数 (1-999)!"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
fi
;;
4)
if [ -z "$PREFIX" ]; then
log_error "请先设置剧集前缀!"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
exit_animation
continue
fi
clear
echo -e "${gl_zi}>>> 预览重命名结果${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e "${gl_bufan}当前设置:${gl_bai}"
echo -e " ${gl_bufan}剧集前缀:${gl_bai} $PREFIX${gl_bai}"
echo -e " ${gl_bufan}起始季号:${gl_bai} S$SEASON${gl_bai}"
echo -e " ${gl_bufan}起始集数:${gl_bai} E$(safe_printf "%02d" "$START_EP")${gl_bai}"
if [ $auto_detected -eq 1 ]; then
echo -e " ${gl_lv}✓ 智能检测已应用${gl_bai}"
fi
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
local current_ep=$START_EP
detection_results=()
local summary_array=()
local summary_count_array=()
echo -e "${gl_bai}重命名预览:${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
for ((i = 0; i < ${#files[@]}; i++)); do
local file="${files[$i]}"
local filename=$(basename -- "$file")
local extension="${filename##*.}"
IFS=':' read -r original_ep original_season type <<<"${episode_info[$i]}"
local formatted_ep=$(safe_printf "%02d" "$current_ep")
local new_name="${PREFIX}-S${SEASON}E${formatted_ep}.${extension}"
local found=0
for j in "${!summary_array[@]}"; do
if [ "${summary_array[j]}" = "$type" ]; then
summary_count_array[j]=$((summary_count_array[j] + 1))
found=1
break
fi
done
if [ $found -eq 0 ]; then
summary_array+=("$type")
summary_count_array+=(1)
fi
echo -e " ${gl_bufan}[$(safe_printf "%02d" $((i + 1)))]${gl_bai}"
echo -e " 原文件: ${gl_hui}$filename${gl_bai}"
echo -e " 识别为: ${type} (E$(safe_printf "%02d" "$original_ep"))${gl_bai}"
echo -e " 新文件: ${gl_lv}$new_name${gl_bai}"
echo ""
detection_results+=("$file:$new_name:$type:$original_ep")
((current_ep++))
done
if [ ${#summary_array[@]} -gt 0 ]; then
echo -e "${gl_bai}识别模式统计:${gl_bai}"
for i in "${!summary_array[@]}"; do
local type="${summary_array[i]}"
local count="${summary_count_array[i]}"
local percentage=$((count * 100 / ${#files[@]}))
echo -e " ${gl_bufan}${type}:${gl_bai} ${count} 文件 (${percentage}%)${gl_bai}"
done
fi
preview_mode=1
preview_done=1 # 标记已完成预览
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
local accurate_count=0
for i in "${!summary_array[@]}"; do
local type="${summary_array[i]}"
if [[ "$type" == "SxE格式" ]] || [[ "$type" == "EP格式" ]] || [[ "$type" == "中文第X集" ]]; then
accurate_count=$((accurate_count + summary_count_array[i]))
fi
done
local accuracy=0
if [ ${#files[@]} -gt 0 ]; then
accuracy=$((accurate_count * 100 / ${#files[@]}))
fi
log_info "预览完成,共 ${gl_lv}${#detection_results[@]} ${gl_bai}个文件"
if [ ${#files[@]} -gt 0 ]; then
log_info "识别准确率: ${gl_lv}${accuracy}${gl_bai}%"
fi
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
;;
5)
if [ $preview_mode -eq 0 ]; then
echo -e ""
log_error "请先预览重命名结果!"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
exit_animation
continue
fi
if [ ${#detection_results[@]} -eq 0 ]; then
echo -e ""
log_error "请先设置剧集前缀,或没有可重命名的文件!"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
exit_animation
continue
fi
clear
echo -e "${gl_zi}>>> 确认重命名${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
local type_array=()
local type_count_array=()
for item in "${detection_results[@]}"; do
IFS=':' read -r old_file new_name type original_ep <<<"$item"
local found=0
for i in "${!type_array[@]}"; do
if [ "${type_array[i]}" = "$type" ]; then
type_count_array[i]=$((type_count_array[i] + 1))
found=1
break
fi
done
if [ $found -eq 0 ]; then
type_array+=("$type")
type_count_array+=(1)
fi
done
echo -e "${gl_bai}重命名统计:${gl_bai}"
echo -e " ${gl_bufan}总文件数:${gl_bai} ${#detection_results[@]}${gl_bai}"
for i in "${!type_array[@]}"; do
local type="${type_array[i]}"
local count="${type_count_array[i]}"
echo -e " ${gl_bufan}${type}:${gl_bai} ${count} 文件${gl_bai}"
done
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
read -r -e -p "$(echo -e "${gl_bai}确定要执行重命名吗? (${gl_lv}y${gl_bai}/${gl_hong}N${gl_bai}): ")" confirm
case "$confirm" in
[Yy])
log_info "开始重命名 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
rename_count=0
local success_count=0
local fail_count=0
for item in "${detection_results[@]}"; do
IFS=':' read -r old_file new_name type original_ep <<<"$item"
if [ -f "$old_file" ]; then
echo -e "${gl_bai}处理: ${gl_hui}$(basename "$old_file")${gl_bai}"
echo -e " 识别: ${type} (E$(safe_printf "%02d" "$original_ep"))${gl_bai}"
if mv -- "$old_file" "./$new_name" 2>/dev/null; then
echo -e " ${gl_lv}✓ 重命名为: $new_name${gl_bai}"
((success_count++))
else
echo -e " ${gl_hong}✗ 重命名失败${gl_bai}"
((fail_count++))
fi
else
echo -e "${gl_huang}⚠ 文件不存在: $(basename "$old_file")${gl_bai}"
((fail_count++))
fi
echo ""
done
rename_count=$success_count
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e "${gl_lv}成功:${gl_bai} $success_count 个文件${gl_bai}"
echo -e "${gl_hong}失败:${gl_bai} $fail_count 个文件${gl_bai}"
log_ok "重命名完成!"
preview_mode=0
auto_detected=0
detection_results=()
# 重命名完成重置状态标记
prefix_set=0
preview_done=0
PREFIX=""
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
;;
[Nn])
log_info "已取消重命名操作"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
;;
*)
handle_y_n
;;
esac
;;
0) exit_script ;;
*) handle_invalid_input ;;
esac
done
}
rename_tv_files_ultimate
创建本地脚本
new_script="new_test.sh"
cat > "$new_script" << 'EOF'
#!/bin/bash
# 粘贴脚本源码
EOF
# 保留本地脚本,去掉 rm -f "$new_script"
chmod +x "$new_script" && ./"$new_script" && rm -f "$new_script"