分享一个原盘的截图和获取 BDINFO 的小工具

1 43~56 分钟
#!/bin/bash

# bd - 蓝光截图和信息提取工具
# 用法: bd <路径> [--count <数量>] [--grid ROWSxCOLS] [--lang LANGUAGE] [--info]

set -e

# 默认配置
COUNT=3
TARGET_DIR=""
MOUNT_POINT="/tmp/bd_mount"
GRID_LAYOUT=""
LANGUAGE="chinese"
OUTPUT_DIR=""
SHOW_INFO=false

# BDInfo 配置
BDINFO_URL="https://github.com/dotnetcorecorner/BDInfo/releases/download/linux-2.0.6/bdinfo_linux_v2.0.6.zip"
MIRRORS=(
    "$BDINFO_URL"
    "https://ghproxy.com/$BDINFO_URL"
    "https://ghfast.top/$BDINFO_URL"
)
INSTALL_DIR="/usr/local/bin"
TEMPDIR=$(mktemp -d)

# 解析参数
while [[ $# -gt 0 ]]; do
    case $1 in
        --count)
            COUNT="$2"
            shift 2
            ;;
        --grid)
            GRID_LAYOUT="$2"
            shift 2
            ;;
        --lang)
            LANGUAGE="$2"
            shift 2
            ;;
        --info)
            SHOW_INFO=true
            shift
            ;;
        *)
            if [[ -z "$TARGET_DIR" ]]; then
                # 支持带引号的路径
                TARGET_DIR="${1//\'/}"
                OUTPUT_DIR="${TARGET_DIR}/screenshots"
            else
                echo "错误: 多余的参数 $1"
                exit 1
            fi
            shift
            ;;
    esac
done

# 创建输出目录
mkdir -p "$OUTPUT_DIR"

# 创建挂载点
mkdir -p "$MOUNT_POINT"

# 安装必要依赖
install_dependencies() {
    local missing=()
    for cmd in ffmpeg curl jq pngquant; do
        if ! command -v $cmd &>/dev/null; then
            missing+=("$cmd")
        fi
    done

    if [ ${#missing[@]} -gt 0 ]; then
        echo "正在安装依赖: ${missing[*]}"
        if command -v apt &>/dev/null; then
            sudo apt install -y "${missing[@]}"
        elif command -v yum &>/dev/null; then
            sudo yum install -y "${missing[@]}"
        else
            echo "请手动安装依赖: ${missing[*]}"
            exit 1
        fi
    fi
}

# 安装BDInfo
install_bdinfo() {
    if ! command -v BDInfo &>/dev/null; then
        echo "正在安装BDInfo..."
        
        for mirror in "${MIRRORS[@]}"; do
            if wget -q "$mirror" -O "$TEMPDIR/bdinfo.zip"; then
                unzip -q -o "$TEMPDIR/bdinfo.zip" -d "$TEMPDIR"
                chmod +x "$TEMPDIR"/BDInfo*
                sudo cp "$TEMPDIR"/BDInfo* "$INSTALL_DIR/"
                rm -rf "$TEMPDIR"
                echo "BDInfo 安装成功!"
                return 0
            fi
        done
        
        echo "错误: 无法下载BDInfo"
        exit 1
    fi
}

# 检测输入类型并返回BDInfo参数
get_input_type() {
    local input="$1"
    
    if [ -f "$input" ]; then
        # 直接传入ISO文件
        echo "-p \"$input\""
    elif [ -d "$input" ]; then
        # 检查是否是BDMV目录
        if [ -d "$input/BDMV" ]; then
            echo "-p \"$input\""
        else
            # 查找目录中的ISO文件
            local iso_file=$(find "$input" -maxdepth 1 -type f \( -iname "*.iso" \) | head -1)
            if [ -n "$iso_file" ]; then
                echo "-p \"$iso_file\""
            else
                echo "错误: 目录中未找到BDMV结构或ISO文件" >&2
                exit 1
            fi
        fi
    else
        echo "错误: 无效的输入路径" >&2
        exit 1
    fi
}

# 优化的BDInfo解析器
parse_bdinfo() {
    awk '
    BEGIN {
        RS = "DISC INFO:"
        max_size = 0
        best_section = ""
    }

    NR > 1 {
        section = "DISC INFO:" $0
        sub(/FILES:.*/, "", section)
        
        if (match(section, /Size:[[:space:]]+([0-9,]+)/)) {
            size_str = substr(section, RSTART+5, RLENGTH-5)
            gsub(/,/, "", size_str)
            size = size_str + 0
            
            if (size > max_size) {
                max_size = size
                best_section = section
            }
        }
    }

    END {
        if (best_section != "") {
            sub(/[[:space:]]+$/, "", best_section)
            print "↓#↓#↓#↓#↓#↓#↓#↓#↓#↓#↓ BDInfo 信息 ↓#↓#↓#↓#↓#↓#↓#↓#↓#↓#↓"
            print best_section
            print "↑#↑#↑#↑#↑#↑#↑#↑#↑#↑#↑ 分割线 ↑#↑#↑#↑#↑#↑#↑#↑#↑#↑#↑"
        } else {
            print "错误: 未找到有效的PLAYLIST信息" > "/dev/stderr"
            exit 1
        }
    }
    '
}

# 提取BD信息
extract_bd_info() {
    local target="$1"
    install_bdinfo
    input_type=$(get_input_type "$target")
    bdinfo_file="$TEMPDIR/bdinfo_$$.txt"

    echo "正在提取BD信息..."
    if eval "BDInfo $input_type -o \"$bdinfo_file\""; then
        cp "$bdinfo_file" "${OUTPUT_DIR}/bdinfo.txt"
        parse_bdinfo < "$bdinfo_file"
        rm -f "$bdinfo_file"
    else
        echo "错误: BDInfo执行失败" >&2
        exit 1
    fi
}

# 清理函数
cleanup() {
    if mountpoint -q "$MOUNT_POINT"; then
        sudo umount "$MOUNT_POINT" 2>/dev/null || true
    fi
    rm -rf "$MOUNT_POINT"
    rm -rf "$TEMPDIR"
}
trap cleanup EXIT


# 压缩PNG图片
compress_png() {
    local file="$1"
    local max_size_mb=10
    local max_size_bytes=$((max_size_mb * 1024 * 1024))
    local temp_file="${file%.*}_compressed.png"
    
    # 获取当前文件大小
    local current_size=$(stat -c%s "$file" 2>/dev/null || echo 0)
    
    if ((current_size <= max_size_bytes)); then
        return 0  # 文件已经小于限制,不需要压缩
    fi
    
    # echo "图片过大($((current_size/1024/1024))MB),进行压缩..."
    
    
    # 方法1: 如果有pngquant,使用它进行强力压缩
    if command -v pngquant &>/dev/null; then
        if pngquant --force --output "$temp_file" --quality 70-80 "$file" 2>/dev/null; then
            local new_size=$(stat -c%s "$temp_file" 2>/dev/null || echo 0)
            if ((new_size > 0 && new_size <= max_size_bytes)); then
                mv "$temp_file" "$file"
                # echo "压缩成功: $((current_size/1024))KB → $((new_size/1024))KB"
                return 0
            fi
        fi
    fi

    # 方法2: 使用ffmpeg调整质量
		# echo "ffmpeg -i '$file' -vcodec png -compression_level 9 -pred mixed -pix_fmt rgba -y '$temp_file'"
		# return 1
    if ffmpeg -i "$file" -vcodec png -compression_level 9 -pred mixed -pix_fmt rgba -y "$temp_file" 2>/dev/null; then
        local new_size=$(stat -c%s "$temp_file" 2>/dev/null || echo 0)
        if ((new_size > 0 && new_size <= max_size_bytes)); then
            mv "$temp_file" "$file"
						# echo new_size: $new_size
            # echo "压缩成功: $((current_size/1024))KB → $((new_size/1024))KB"
            return 0
        fi
    fi
    
    # 所有压缩方法都失败
    rm -f "$temp_file"
    echo "警告: 无法将图片压缩到10MB以下,尝试上传原图"
    return 1
}

# 上传图片到图床
upload_to_pixhost() {
    local file="$1"
    local max_size_mb=10
    local max_retry=3
    local retry_count=0

    # 先检查文件大小,如果过大则压缩
    local size=$(stat -c%s "$file" 2>/dev/null || echo 0)
    if ((size > max_size_mb * 1024 * 1024)); then
        if ! compress_png "$file"; then
            echo "压缩失败,跳过上传"
            return 1
        fi
    fi

    while ((retry_count < max_retry)); do
        # 重新检查文件大小(压缩后)
        local size=$(stat -c%s "$file" 2>/dev/null || echo 0)
        if ((size > max_size_mb * 1024 * 1024)); then
            echo "文件仍然过大($((size/1024/1024))MB),跳过上传"
            return 1
        fi

        local response=$(curl -s -F "name=$(basename "$file")" \
             -F "ajax=yes" -F "content_type=0" -F "file=@$file" \
             "https://pixhost.to/new-upload/")

        if [ -z "$response" ]; then
            echo "失败(空响应)"
        else
            local error=$(echo "$response" | jq -r '.error.description' 2>/dev/null)
            if [ "$error" != "null" ] && [ -n "$error" ]; then
                echo "失败($error)"
            else
                local url=$(echo "$response" | jq -r '.show_url' | sed 's|\\||g;s|pixhost\.to/show|img1.pixhost.to/images|')
                echo "[img]$url[/img]"
                return 0
            fi
        fi

        ((retry_count++))
        sleep 1
    done

    echo "上传失败: 超过最大重试次数"
    return 1
}

# 获取视频时长(秒)
get_duration() {
    local input="$1"
    local duration=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$input" 2>/dev/null)
    echo "$duration" | awk '{print int($1)}'
}

# 获取随机时间点(排除前后2分钟)
get_random_time() {
    local duration="$1"
    local start_time=120  # 2分钟 = 120秒
    local end_time=$((duration - 120))
    
    if ((end_time <= start_time)); then
        echo "0"
        return
    fi
    
    local random_time=$((RANDOM % (end_time - start_time) + start_time))
    echo "$random_time"
}

# 获取字幕流索引
get_subtitle_index() {
    local input="$1"
    local language="$2"
    
    # 获取所有字幕流信息
    local subtitle_info=$(ffprobe -v error -select_streams s -show_entries stream=index:stream_tags=language -of csv=p=0 "$input" 2>/dev/null)
    
    if [[ -z "$subtitle_info" ]]; then
        echo ""  # 没有字幕流
        return
    fi
    
    # 查找指定语言的字幕流(不区分大小写)
    while IFS= read -r line; do
        local index=$(echo "$line" | cut -d',' -f1)
        local lang=$(echo "$line" | cut -d',' -f2 | tr '[:upper:]' '[:lower:]')
        
        if [[ "$lang" == *"${language,,}"* ]]; then
            echo "$index"
            return
        fi
    done <<< "$subtitle_info"
    
    echo ""  # 没有找到指定语言的字幕
}

# 截图函数(带字幕支持)
capture_screenshot() {
    local input="$1"
    local output="$2"
    local duration=$(get_duration "$input")
    
    if ((duration == 0)); then
        echo "错误: 无法获取视频时长"
        return 1
    fi
    
    local time_point=$(get_random_time "$duration")
    local subtitle_index=$(get_subtitle_index "$input" "$LANGUAGE")
    
    echo "时间点: ${time_point}s, 字幕流: ${subtitle_index:-无}"
    
    if [[ -n "$subtitle_index" ]]; then
        # 带字幕截图(单张不缩放)
        if [[ -z "$GRID_LAYOUT" ]]; then
            ffmpeg -ss "$time_point" -i "$input" -vf "subtitles=$input:si=$subtitle_index" -vframes 1 -c:v png -compression_level 6 -y "$output" 2>/dev/null
        else
            # 拼图模式需要缩放
            ffmpeg -ss "$time_point" -i "$input" -vf "subtitles=$input:si=$subtitle_index,scale=512:-1" -vframes 1 -c:v png -compression_level 6 -y "$output" 2>/dev/null
        fi
    else
        # 无字幕截图(单张不缩放)
        if [[ -z "$GRID_LAYOUT" ]]; then
            ffmpeg -ss "$time_point" -i "$input" -vframes 1 -c:v png -compression_level 6 -y "$output" 2>/dev/null
        else
            # 拼图模式需要缩放
            ffmpeg -ss "$timestamp" -i "$input" -vf "scale=512:-1" -vframes 1 -c:v png -compression_level 6 -y "$output" 2>/dev/null
        fi
    fi
    
    return $?
}

# 处理BDMV格式
process_bdmv() {
    local bdmv_dir="$1"
    local stream_dir="$bdmv_dir/BDMV/STREAM"
    
    if [[ ! -d "$stream_dir" ]]; then
        echo "错误: 找不到 BDMV/STREAM 目录"
        return 1
    fi
    
    # 找到最大的.m2ts文件
    local largest_file=$(find "$stream_dir" -iname "*.m2ts" -type f -exec du -b {} \; | sort -nr | head -1 | cut -f2)
    
    if [[ -z "$largest_file" ]]; then
        echo "错误: 找不到.m2ts文件"
        return 1
    fi
    
    echo "使用文件: $(basename "$largest_file")"
    process_video_file "$largest_file"
}

# 处理ISO格式
process_iso() {
    local iso_file="$1"
    
    # 挂载ISO
    if ! sudo mount -o loop "$iso_file" "$MOUNT_POINT" 2>/dev/null; then
        echo "错误: 无法挂载ISO文件"
        return 1
    fi
    
    # 检查是否是BDMV或DVD
    if [[ -d "$MOUNT_POINT/BDMV" ]]; then
        process_bdmv "$MOUNT_POINT"
    elif [[ -d "$MOUNT_POINT/VIDEO_TS" ]]; then
        process_dvd "$MOUNT_POINT"
    else
        echo "错误: 无法识别ISO格式"
        return 1
    fi
    
    # 卸载ISO
    sudo umount "$MOUNT_POINT"
}

# 处理DVD格式
process_dvd() {
    local dvd_dir="$1"
    local video_ts_dir="$dvd_dir/VIDEO_TS"
    
    if [[ ! -d "$video_ts_dir" ]]; then
        echo "错误: 找不到 VIDEO_TS 目录"
        return 1
    fi
    
    # 找到最大的.VOB文件
    local largest_file=$(find "$video_ts_dir" -name "*.VOB" -type f -exec du -b {} \; | sort -nr | head -1 | cut -f2)
    
    if [[ -z "$largest_file" ]]; then
        echo "错误: 找不到.VOB文件"
        return 1
    fi
    
    echo "使用文件: $(basename "$largest_file")"
    process_video_file "$largest_file"
}

# 使用FFmpeg创建拼图
create_grid_with_ffmpeg() {
    local input_files=("$@")
    local grid_file="${OUTPUT_DIR}/${base_name}-grid.png"
    
    # echo "创建拼图: $grid_file"
    
    # 检查输入文件是否存在
    local valid_files=()
    for file in "${input_files[@]}"; do
        if [[ -f "$file" ]]; then
            valid_files+=("$file")
        fi
    done
    
    if [[ ${#valid_files[@]} -eq 0 ]]; then
        echo "错误: 没有有效的截图文件用于拼图"
        return 1
    fi
    
    # 解析网格布局
    local rows=2
    local cols=2
    if [[ -n "$GRID_LAYOUT" ]]; then
        rows=$(echo "$GRID_LAYOUT" | cut -d'x' -f1)
        cols=$(echo "$GRID_LAYOUT" | cut -d'x' -f2)
    fi
    
    # 使用FFmpeg创建拼图 - 使用不同的方法
    if [[ ${#valid_files[@]} -eq 1 ]]; then
        # 如果只有一张图,直接复制
        cp "${valid_files[0]}" "$grid_file"
    else
        # 使用filter_complex拼接多张图片
        local filter_complex=""
        for ((i=0; i<${#valid_files[@]}; i++)); do
            filter_complex+="[${i}:v]"
        done
        filter_complex+="tile=${cols}x${rows}:margin=5:padding=5:color=white[out]"
        
        # 构建输入文件参数
        local input_args=()
        for file in "${valid_files[@]}"; do
            input_args+=("$file")
        done
        # 执行FFmpeg拼图
				ffmpeg -loglevel error -i "concat:$(IFS='|'; echo "${input_args[*]}")" -filter_complex "[0:v]tile=${cols}x${rows}[v]" -map "[v]" -y "$grid_file" 2>/dev/null
        if [[ $? -ne 0 ]]; then
            echo "拼图创建失败,尝试备用方法..."
            # 备用方法:使用montage(如果可用)
            if command -v montage &>/dev/null; then
                montage "${valid_files[@]}" -geometry 512x -tile "${cols}x${rows}" -background white "$grid_file"
            else
                echo "错误: 无法创建拼图"
                return 1
            fi
        fi
    fi
    
    if [[ ! -f "$grid_file" ]]; then
        echo "错误: 拼图文件未生成"
        return 1
    fi
    
    # 检查文件大小
    local size=$(stat -c%s "$grid_file" 2>/dev/null || echo 0)
    if ((size == 0)); then
        echo "错误: 生成的拼图文件为空"
        return 1
    fi
    
    # echo "拼图创建成功: $grid_file ($((size/1024))KB)"
    
    # 上传拼图
    # echo "↓#↓#↓#↓#↓#↓#↓#↓#↓#↓#↓ 拼图 ↓#↓#↓#↓#↓#↓#↓#↓#↓#↓#↓"
    if ! upload_to_pixhost "$grid_file"; then
        echo "拼图上传失败,本地文件保留: $grid_file"
    fi
    # echo "↑#↑#↑#↑#↑#↑#↑#↑#↑#↑#↑ 分割线 ↑#↑#↑#↑#↑#↑#↑#↑#↑#↑#↑"
    
    # 删除临时单张图片,保留拼图文件
    rm -f "${input_files[@]}"
}

# 处理视频文件并截图
process_video_file() {
    local video_file="$1"
    local base_name=$(basename "$TARGET_DIR")
    local screenshot_files=()
    
    # 计算需要截图的帧数
    local total_frames=$COUNT
    if [[ -n "$GRID_LAYOUT" ]]; then
        local rows=$(echo "$GRID_LAYOUT" | cut -d'x' -f1)
        local cols=$(echo "$GRID_LAYOUT" | cut -d'x' -f2)
        total_frames=$((rows * cols))
				# echo "total_frames: $total_frames"
    fi
    
    local duration=$(get_duration "$video_file")
    local margin=120  # 前后2分钟
    local available_duration=$((duration - 2 * margin))
    local interval=$((available_duration / total_frames))
    

    echo "↓#↓#↓#↓#↓#↓#↓#↓#↓#↓#↓ 截图 ↓#↓#↓#↓#↓#↓#↓#↓#↓#↓#↓"
    for ((i=0; i<total_frames; i++)); do
        local timestamp=$((margin + i * interval))
        local outfile="${OUTPUT_DIR}/${base_name}-$(printf "%02d" $((i+1))).png"
        screenshot_files+=("$outfile")
        
        while true; do
            # echo "截图中: $(basename "$outfile") (${timestamp}s)"
            
            local subtitle_index=$(get_subtitle_index "$video_file" "$LANGUAGE")
            
            if [[ -n "$subtitle_index" ]]; then
                # 带字幕截图
                if [[ -z "$GRID_LAYOUT" ]]; then
                    # 单张不缩放
                    ffmpeg -ss "$timestamp" -i "$video_file" \
                        -vf "subtitles=$video_file:si=$subtitle_index" \
                        -vframes 1 -c:v png -compression_level 6 -y "$outfile" 2>/dev/null
                else
                    # 拼图需要缩放
                    ffmpeg -ss "$timestamp" -i "$video_file" \
                        -vf "subtitles=$video_file:si=$subtitle_index,scale=512:-1" \
                        -vframes 1 -c:v png -compression_level 6 -y "$outfile" 2>/dev/null
                fi
            else
                # 无字幕截图
                if [[ -z "$GRID_LAYOUT" ]]; then
                    # 单张不缩放
                    ffmpeg -ss "$timestamp" -i "$video_file" \
                        -vframes 1 -c:v png -compression_level 6 -y "$outfile" 2>/dev/null
                else
                    # 拼图需要缩放
                    ffmpeg -ss "$timestamp" -i "$video_file" \
                        -vf "scale=512:-1" -vframes 1 -c:v png -compression_level 6 -y "$outfile" 2>/dev/null
                fi
            fi
            
            if [[ $? -eq 0 && -f "$outfile" ]]; then
                local size=$(stat -c%s "$outfile" 2>/dev/null || echo 0)
								if ((size == 0)); then
										echo "截图文件为空,重新截取..."
										rm -f "$outfile"
										continue
								fi
                # if ((size > 10 * 1024 * 1024)); then
                #     echo "文件过大($((size/1024/1024))MB),重新截取..."
                #     rm -f "$outfile"
                #     continue
                # elif ((size == 0)); then
                #     echo "截图文件为空,重新截取..."
                #     rm -f "$outfile"
                #     continue
                # fi
                
                # 如果是单张截图模式,直接上传
                if [[ -z "$GRID_LAYOUT" ]]; then
                    upload_to_pixhost "$outfile"
                    # 单张截图也保留文件
                    # echo "截图已保存: $outfile"
                fi
                
                break
            else
                echo "截图失败,重试..."
                sleep 1
            fi
        done
    done
    
    # 如果是拼图模式,创建拼图
    if [[ -n "$GRID_LAYOUT" ]]; then
        create_grid_with_ffmpeg "${screenshot_files[@]}"
    fi
    echo "↑#↑#↑#↑#↑#↑#↑#↑#↑#↑#↑ 分割线 ↑#↑#↑#↑#↑#↑#↑#↑#↑#↑#↑"
}

# 查找ISO文件(不区分大小写)
find_iso_file() {
    local dir="$1"
    # 查找不区分大小写的ISO文件
    find "$dir" -maxdepth 1 -type f -iname "*.iso" | head -1
}

# 主函数
main() {
    if [[ -z "$TARGET_DIR" ]]; then
        echo "错误: 请指定原盘路径"
        echo "用法: bd <路径> [--count <数量>] [--grid ROWSxCOLS] [--lang LANGUAGE] [--info]"
        exit 1
    fi
    
    install_dependencies
    
		# echo GRID_LAYOUT:$GRID_LAYOUT
		# echo COUNT:$COUNT
		# echo SHOW_INFO:$SHOW_INFO
		# echo TARGET_DIR:$TARGET_DIR
		# echo OUTPUT_DIR:$OUTPUT_DIR
		# echo LANGUAGE:$LANGUAGE
		# echo MOUNT_POINT:$MOUNT_POINT
		# echo TEMPDIR:$TEMPDIR

    # 判断是否需要截图
    local need_screenshot=false
    # 只要 --count 被指定(即使等于3),或者 --grid 被指定,就认为要截图
    if [[ -n "$GRID_LAYOUT" || -n "$COUNT" ]]; then
        need_screenshot=true
    fi

    # 显示BD信息
    if [[ "$SHOW_INFO" == true ]]; then
        extract_bd_info "$TARGET_DIR"
        echo ""
    fi
		
    echo need_screenshot:$need_screenshot
    # 如果只指定了 --info 且没有截图参数,则只显示信息后退出
    if [[ "$SHOW_INFO" == true && "$need_screenshot" == false ]]; then
        echo "只显示BD信息,不进行截图"
        exit 0
    fi

    echo "处理原盘: $TARGET_DIR"
    echo "截图数量: $COUNT"
    echo "输出目录: $OUTPUT_DIR"
    
    if [[ -n "$GRID_LAYOUT" ]]; then
        echo "拼图模式: $GRID_LAYOUT"
    fi
    echo "字幕语言: $LANGUAGE"
    
    # 首先检查是否是ISO文件
    local iso_file=$(find_iso_file "$TARGET_DIR")
    if [[ -n "$iso_file" ]]; then
        echo "检测到ISO文件: $(basename "$iso_file")"
        process_iso "$iso_file"
    elif [[ -d "$TARGET_DIR/BDMV" ]]; then
        echo "检测到BDMV格式"
        process_bdmv "$TARGET_DIR"
    elif [[ -d "$TARGET_DIR/VIDEO_TS" ]]; then
        echo "检测到DVD格式"
        process_dvd "$TARGET_DIR"
    else
        echo "错误: 无法识别原盘格式"
        echo "支持的格式: BDMV文件夹、VIDEO_TS文件夹或ISO文件"
        exit 1
    fi
    
    echo "处理完成!"
}

# 运行主函数
main "$@"