#!/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 "$@"