保存时间:2026/4/3 03:29:58
ls command, you can combine ls with formatting options and text-processing tools like awk to extract the time-related fields. Here's how to do it:ls -l command shows detailed file information, including the modification time (last time content was changed). Use awk to extract just the time fields:ls -l [filename] | awk '{print $6, $7, $8}'
ls -l document.txt | awk '{print $6, $7, $8}'
# Output: Jun 15 09:45
--full-time with ls:ls -l --full-time [filename] | awk '{print $6, $7}'
ls -l --full-time document.txt | awk '{print $6, $7}'
# Output: 2024-06-15 09:45:30.123456789
-uls -lu [filename] | awk '{print $6, $7, $8}'
-cls -lc [filename] | awk '{print $6, $7, $8}'
[filename] with your target file name.ls may show the year instead of the time (e.g., Jun 15 2023), but the command still works.timestamp=$(ls $local_file | awk '{print $6, $7, $8}')。这里的问题可能出在ls命令的输出格式上。因为ls的输出格式可能因系统、环境变量(比如 LC_TIME)或文件数量不同而变化,导致列数不确定。比如,当文件名包含空格时,ls的输出可能会被分割错误,而且不同系统下ls显示的日期时间列可能不是第 6、7、8 列。stat命令,因为stat可以更可靠地获取文件的元数据,包括修改时间,而且格式可以自定义,不受ls输出格式的影响。stat命令参数可能不同。用户的脚本看起来是在 Linux 上运行的(因为用了s3cmd,通常在 Linux 环境),Linux 的stat支持--format参数来指定输出格式。stat --format=%y可以获取最后修改时间,格式是YYYY-MM-DD HH:MM:SS。如果只需要日期和时间部分,可以截取一下,比如stat --format=%y "$local_file" | cut -d. -f1,这样可以去掉后面的小数秒。ls命令,改用stat来获取时间戳。原来的ls方式不可靠,导致 timestamp 为空,因为可能列数不对或者ls输出被解析错误。timestamp=(ls ...)这里用了括号,其实应该是timestamp=$(...),不过用户已经是这样写的,可能只是笔误在描述里。主要问题还是ls的不可靠性。stat命令来获取文件的修改时间,确保格式正确,这样 timestamp 变量就能正确获取值了。需要修改获取 timestamp 的那一行,用stat替代ls,并正确设置格式。ls 命令获取时间戳的方式不可靠,ls 的输出格式会受系统设置、语言环境、文件名是否包含空格等因素影响,导致 awk 无法正确提取第6、7、8列(实际可能根本不是时间列)。stat 命令可靠获取文件时间戳stat 命令可以直接读取文件的元数据(包括修改时间),格式可控且稳定。修改获取时间戳的代码如下:# 替换原来的 timestamp 赋值行
# 注意:Linux 系统的 stat 用法,macOS 需用 stat -f "%Sm" 格式
timestamp=$(stat --format="%y" "$local_file" | cut -d. -f1) # %y 表示最后修改时间,格式为 "YYYY-MM-DD HH:MM:SS"
stat 命令:stat --format="%y" 直接获取文件的最后修改时间,格式固定为 YYYY-MM-DD HH:MM:SS.xxx(包含毫秒)。cut -d. -f1 用于截取小数点前的日期时间部分,最终格式为 YYYY-MM-DD HH:MM:SS。"$local_file" 加双引号,防止文件名包含空格时解析错误。# we always need to write link to file
timestamp=$(stat --format="%y" "$local_file" | cut -d. -f1) # 可靠获取最后修改时间
echo "time stamp = $timestamp"
safe_exec "echo '<li><a href=\"$s3_url\">$filename ($timestamp)</a></li>' >> $LINK_FILE" # 写链接
stat 命令参数不同,需改用:timestamp=$(stat -f "%Sm" -t "%Y-%m-%d %H:%M:%S" "$local_file")
stat 的格式参数即可(例如 %Y-%m-%d 只保留年月日)。stat $filename 可以间接判断文件是否存在,但它的主要功能是获取文件的元数据(而非专门用于存在性检查)。其判断逻辑基于 命令的退出状态码:stat 命令的返回值规则stat 命令成功执行,退出状态码为 0(在 shell 中,0 表示成功)。stat 命令执行失败,退出状态码为 非0(通常是 1 或 2)。stat 的返回值,可以判断文件是否存在(前提是排除权限问题)。stat 的返回值?$? 中。也可以直接在条件语句(如 if)中使用命令,根据其返回值执行不同逻辑。$? 查看返回值filename="test.txt"
# 执行 stat 命令(重定向输出到/dev/null,避免干扰)
stat "$filename" >/dev/null 2>&1
# 检查返回值
if [ $? -eq 0 ]; then
echo "文件 $filename 存在"
else
echo "文件 $filename 不存在或无法访问"
fi
if 中使用 stat(更简洁)filename="test.txt"
# 直接用 stat 作为 if 的条件(返回值 0 则进入 then 分支)
if stat "$filename" >/dev/null 2>&1; then
echo "文件 $filename 存在"
else
echo "文件 $filename 不存在或无法访问"
fi
>/dev/null 2>&1 用于屏蔽 stat 的正常输出和错误输出(只关注返回值,不显示多余信息)。"$filename" 必须加双引号,防止文件名包含空格、星号等特殊字符时解析错误。stat 可以判断文件存在,但 Linux 中更专门的工具是 test 命令(或其简写 [ ]/[[ ]]),语法更简洁且专为条件判断设计:filename="test.txt"
# 检查普通文件是否存在(最常用)
if [ -f "$filename" ]; then
echo "文件 $filename 存在"
else
echo "文件 $filename 不存在"
fi
-f 表示检查是否为 普通文件(排除目录、设备文件等);-e 检查文件/目录是否存在,-d 检查是否为目录。stat $filename 可以判断文件是否存在,通过返回值 0(存在)和非0(不存在)区分。$? 变量或直接在 if 语句中使用 stat 命令来获取返回值。[ -f "$filename" ],专为文件存在性检查设计,更简洁可靠。. 后面的部分。${变量名##*.}## 表示从字符串开头开始,最大程度匹配并删除指定的模式(这里的模式是 *.,即任意字符 + 点)。. 后面的部分(即后缀)。filename 存储文件名,通过以下方式获取后缀:# 定义测试文件名
filename="document.pdf"
echo ${filename##*.} # 输出:pdf
filename="archive.tar.gz" # 多个点的情况
echo ${filename##*.} # 输出:gz(只取最后一个点后的部分)
filename="image" # 无后缀(无点)
echo ${filename##*.} # 输出:image(无点时返回原文件名)
filename=".bashrc" # 以点开头(隐藏文件)
echo ${filename##*.} # 输出:bashrc(忽略开头的点,取第一个点后的部分)
.:filename="notes.txt"
if [[ "$filename" == *.* ]]; then
# 存在点,提取后缀
extension="${filename##*.}"
else
# 不存在点,后缀为空
extension=""
fi
echo "后缀:$extension" # 输出:后缀:txt
${filename##*.}(适用于大多数场景)。[[ "$filename" == *.* ]] 判断后再提取。sed/awk 等外部命令,效率更高。${变量名%.*}% 表示从字符串末尾开始,最小程度匹配并删除指定的模式(这里的模式是 .*,即点 + 任意字符)。. 及其后面部分(即不含后缀的文件名)。filename 存储文件名,通过以下方式获取不含后缀的部分:# 基本用法
filename="document.pdf"
echo ${filename%.*} # 输出:document
# 多个点的情况(如.tar.gz)
filename="archive.tar.gz"
echo ${filename%.*} # 输出:archive.tar(只去掉最后一个点及后面部分)
# 无后缀(无点)
filename="readme"
echo ${filename%.*} # 输出:readme(无变化)
# 以点开头的隐藏文件(无后缀)
filename=".bashrc"
echo ${filename%.*} # 输出:.bashrc(无变化,因为没有"点+后缀"结构)
# 文件名本身就是后缀(如".gitignore")
filename=".gitignore"
echo ${filename%.*} # 输出:.gitignore(同上,保留原文件)
/home/user/file.txt),需要先提取纯文件名,再去掉后缀:filepath="/home/user/report.pdf"
# 先提取文件名(去掉路径)
filename=$(basename "$filepath") # 得到:report.pdf
# 再去掉后缀
name_without_ext="${filename%.*}"
echo $name_without_ext # 输出:report
${filename%.*}(适用于大多数场景)。basename 提取纯文件名,再用上述方法处理。sed/awk 等外部命令更高效。if 语句在 shell 中是条件为真(命令返回值 0)时执行 then 分支,而 stat 命令的返回值规则是:stat 成功 → 返回值 0 → if 认为条件为真 → 执行 then 分支stat 失败 → 返回值非0 → if 认为条件为假 → 执行 else 分支if stat "$mp3_filename" >/dev/null 2>&1; then
echo " not exists"; # 逻辑反了!stat成功时(文件存在)会走到这里
else
echo "exists"; # stat失败时(文件不存在)会走到这里
fi
stat 失败(返回非0),所以 if 条件为假,执行 else 分支,输出了 exists(这显然与你的文字描述相反)。if stat "$mp3_filename" >/dev/null 2>&1; then
echo "exists"; # 文件存在时执行
else
echo "not exists"; # 文件不存在时执行
fi
! 取反(推荐,更直观):if ! stat "$mp3_filename" >/dev/null 2>&1; then
echo "not exists"; # 文件不存在时执行
else
echo "exists"; # 文件存在时执行
fi
[ -f ] 更专门用于检查文件存在,逻辑更清晰:if [ -f "$mp3_filename" ]; then
echo "exists"
else
echo "not exists"
fi
not exists。if 语句的判断逻辑与返回值的关系是:只有当命令返回值为 0 时,if 才认为条件为“真”,会执行 then 分支;返回值为非 0 时(包括 1),if 认为条件为“假”,会执行 else 分支。stat "$mp3_filename" 执行失败,返回值是 1(非 0)。if 认为条件为“假”,所以会跳过 then 分支,执行 else 分支。if stat "$mp3_filename" >/dev/null 2>&1; then
echo " not exists"; # then分支(条件为真时执行)
else
echo "exists"; # else分支(条件为假时执行)
fi
stat 返回 1(条件为假),所以会执行 else 分支的 echo "exists",这就是为什么你得到了不符合预期的结果——本质是 then 和 else 分支的逻辑写反了。| 文件状态 | stat 返回值 | if 条件判断 | 应该执行的分支 |
|---|---|---|---|
| 存在 | 0 | 真(true) | then 分支(输出 "exists") |
| 不存在 | 1(非 0) | 假(false) | else 分支(输出 "not exists") |
# 正确逻辑:文件存在则进入then分支,否则进入else分支
if stat "$mp3_filename" >/dev/null 2>&1; then
echo "exists"; # 文件存在时执行(返回值0,条件为真)
else
echo "not exists"; # 文件不存在时执行(返回值1,条件为假)
fi
else 分支,输出 not exists,符合预期。if 语句的判断逻辑非常明确:仅当命令的返回值为 0 时,才会认为条件为“真”(true)并执行 then 分支;返回值为非 0(1-255)时,都认为条件为“假”(false)并执行 else 分支。True/False 判断)不同,shell 完全依赖命令退出状态码来决定 if 的执行分支:0:表示命令“成功执行”,if 判定为“真”(进入 then)。0:表示命令“执行失败”(无论具体是 1、2 还是其他值),if 判定为“假”(进入 else)。stat 命令为例:stat 成功执行 → 返回 0 → if 认为“真” → 走 then 分支。stat 执行失败 → 返回 1(非 0) → if 认为“假” → 走 else 分支。then 分支里,而实际需要的是相反的逻辑。if 只认 0 为真,其他都是假,这是必须记住的基础规则。/ 及其前面的部分,或者仅获取最后一个 / 所在的位置。以下是具体实现方法:/ 及其前面的所有内容/ 左侧的完整路径(包括该 /),可以使用 ${path%/*}/ 实现:path="/home/nick/.wine/drive_c/users/nick/Documents/wechat/mp3/2084.mp3"
# 获取最后一个 / 及其前面的内容
last_slash_part="${path%/*}/"
echo "$last_slash_part"
# 输出:/home/nick/.wine/drive_c/users/nick/Documents/wechat/mp3/
${path%/*} 表示从字符串末尾开始,删除最短匹配的 /*(即最后一个 / 及其后面的所有内容),然后手动添加一个 /,最终得到包含最后一个 / 的完整路径前缀。//,可以结合字符串长度和截取实现:path="/home/user/docs/"
# 获取最后一个字符
last_char="${path: -1}"
if [ "$last_char" = "/" ]; then
echo "路径以 / 结尾"
else
echo "路径不以 / 结尾"
fi
# 输出:路径以 / 结尾
${path: -1} 表示截取字符串的最后一个字符,直接判断是否为 /。/ 右侧的文件名(反向应用)/ 后面的内容(即文件名或最后一级目录),可以用 ${path##*/},间接反推出最后一个 / 的位置:path="/home/nick/file.txt"
# 获取最后一个 / 后面的内容(文件名)
filename="${path##*/}"
echo "$filename" # 输出:file.txt
# 结合方法1,可分离路径和文件名
dir_path="${path%/*}/"
echo "$dir_path" # 输出:/home/nick/
/ 的路径前缀:${path%/*}//:${path: -1}${path%%/*}/ 的作用是从字符串开头开始,删除最长匹配的 /* 模式,但这通常不是获取最后一个 / 的正确方式,反而会得到与预期相反的结果。%% 是最长前缀匹配删除,规则是:/*(即第一个 / 及其后面的所有内容)path="/home/nick/docs/file.txt"
# 使用 %%/*
result="${path%%/*}/"
echo "$result" # 输出://
${path%%/*} 会删除从第一个 / 开始的所有内容(即删除 /home/nick/docs/file.txt),只留下空字符串,再加上 / 后变成 //。${path%%/*}/ 不能用于获取最后一个 /,它的作用是删除从第一个 / 开始的所有内容,通常用于提取不含路径的字符串(但很少这样用)。/ 及其前面的路径,正确的写法是使用 %(最短后缀匹配):path="/home/nick/docs/file.txt"
result="${path%/*}/" # 注意是单个 %
echo "$result" # 输出:/home/nick/docs/
%% 处理开头的匹配,% 处理结尾的匹配,获取路径最后一个 / 需用 % 而非 %%。./uploader.sh: line 1: #!/bin/bash: No such file or directory# 转换文件格式为Unix格式
dos2unix uploader.sh
# 或者手动重新创建第一行
sed -i '1s/^.*#/#/' uploader.sh
find | sort | while 管道被意外中断find "$LOCAL_DIR" -maxdepth 1 -type f | sort -V | while read -r local_file; do
# 处理文件的逻辑
done
while循环如果遇到命令执行失败(比如stat、s3cmd等命令报错导致非零退出码),会导致循环提前终止。if stat \"$mp3_filename\" >/dev/null 2>&1; then
stat命令参数被错误地用\"(转义双引号)包裹了,导致实际执行的命令是:stat ""/home/nick/.../2084_1.mp3"" # 路径前后多了双引号,导致stat命令报错
stat会因“文件路径不存在”而返回非零退出码,而脚本开头设置了set -e(遇到错误立即退出),因此管道循环会直接中断,导致后续文件不再处理。stat命令的引号问题if stat \"$mp3_filename\" >/dev/null 2>&1; then
if stat "$mp3_filename" >/dev/null 2>&1; then
$mp3_filename本身已经会被shell自动处理空格等特殊字符,外层加""即可,无需额外转义。set -e触发退出,建议:set -e(在脚本开头改为# set -e),让脚本执行完所有文件并输出完整错误信息DRY_RUN=true时的safe_exec是否有其他命令格式错误(比如路径包含空格但未用引号包裹)ffmpeg、s3cmd等工具是否正确安装且在环境变量中./uploader.sh: line 1: #!/bin/bash: No such file or directory
dos2unix转换格式:dos2unix uploader.sh
dos2unix,可以用sed处理:sed -i 's/\r$//' uploader.sh
2084_3.mp4时中断,且路径异常:local_file: me/nick/.wine/drive_c/users/nick/Documents/wechat/2084_3.mp4
/home/nick/...,这里缺失了/home/,导致ffmpeg命令失败。可能原因:find命令遍历到了一个路径不完整的文件(可能是损坏的软链接或临时文件)。 find命令后过滤无效路径,修改遍历部分:find "$LOCAL_DIR" -maxdepth 1 -type f | sort -V | while read -r local_file; do
# 新增:检查路径是否合法(以/开头且文件存在)
if [[ ! "$local_file" =~ ^/ || ! -f "$local_file" ]]; then
echo "跳过无效文件:$local_file"
continue
fi
# 原逻辑继续...
echo "local_file: $local_file"
# ...
done
/home/nick/.wine/drive_c/users/nick/Documents/wechat/目录下是否有异常文件或软链接,删除或修复无效项。set -e后添加set -x(调试模式),更清晰地看到命令执行过程。me/nick/.wine/...,而正确路径应该是/home/nick/...,缺失了/home/部分。find "$LOCAL_DIR" -maxdepth 1 -type f来获取文件,LOCAL_DIR是/home/nick/.wine/drive_c/users/nick/Documents/wechat,理论上find应该返回以这个路径开头的文件。me/nick/...,这可能是因为find命令返回的路径中,LOCAL_DIR的/home/被错误替换成了me/?或者存在相对路径的问题?不过LOCAL_DIR是绝对路径,find应该返回绝对路径才对。local_file的地方,filename=$(basename "$local_file")这一步应该没问题,basename会提取文件名。但问题可能出在find命令本身,或者目录中存在异常的文件 / 软链接。LOCAL_DIR目录下有一个名为me的子目录,或者有软链接指向me/nick/...,find可能会返回这样的路径。但LOCAL_DIR设置的是绝对路径,find默认应该返回绝对路径,除非有特殊情况,比如LOCAL_DIR本身是相对路径,但这里是绝对的。local_file是通过find获取的,用read -r应该能正确处理。不过检查脚本中find的输出是否被正确处理,是否有路径被截断的情况。sort -V命令导致的问题?但sort -V是按版本号排序,通常不会改变路径本身。../等相对路径字符,导致find返回的路径被解析为相对路径。比如,如果LOCAL_DIR下有一个文件名为../me/nick/...,那么find可能会返回相对路径,但LOCAL_DIR是绝对路径,find应该会解析为绝对路径。LOCAL_DIR的实际路径可能有问题,比如存在软链接指向错误的位置?比如/home/nick被软链接到me/nick,但这种情况较少见。mp3_filename时,用${LOCAL_DIR}/mp3/$mp3_name,LOCAL_DIR是绝对路径,所以mp3_filename应该是绝对路径,这部分看起来没问题。LOCAL_DIR目录下存在路径异常的文件(如包含../的文件名,导致find返回相对路径);2. find命令的输出被意外修改(比如环境变量或别名影响);3. 脚本中存在不可见字符(如之前的 BOM 头问题未完全解决,影响路径解析)。LOCAL_DIR下的文件,用find "$LOCAL_DIR" -maxdepth 1 -type f直接执行,看输出是否有异常路径,排查是否有异常文件或软链接。同时检查字符串处理部分是否有疏漏,比如是否正确使用引号包裹变量,避免路径中的空格等问题导致截断。local_file: me/nick/.wine/...)来看,文件名异常(路径缺失/home/)很可能是文件路径解析或遍历逻辑出了问题,而非单纯的字符串操作错误。具体原因可以从以下几个方面排查:find命令返回了异常路径find "$LOCAL_DIR" -maxdepth 1 -type f遍历文件,LOCAL_DIR被定义为绝对路径:LOCAL_DIR="/home/nick/.wine/drive_c/users/nick/Documents/wechat"find对绝对路径执行时,返回的local_file也应该是绝对路径(以/home/nick/...开头)。但日志中出现了me/nick/...,说明find返回了相对路径,这可能是因为:LOCAL_DIR目录下存在异常文件 / 软链接:比如目录中存在一个名为../me/nick/...的文件或损坏的软链接,find遍历到它时,会解析为相对路径(me/nick/...是../me/nick/...的简化结果)。find命令被意外修改:比如系统中存在find的别名或函数,导致其输出路径被截断 / 修改(可能性较低,但可排查)。filename=$(basename "$local_file")、mp3_name=${filename%%.mp4}.mp3)本身逻辑是对的,但如果local_file本身是异常路径(如相对路径),后续的路径拼接会继承错误:例如,当local_file是me/nick/...时,mp3_filename=${LOCAL_DIR}/mp3/$mp3_name会变成/home/nick/.../mp3/2084_3.mp3(看似正确),但ffmpeg -i $local_file会因local_file路径错误而失败。local_file是否为绝对路径且实际存在,跳过无效文件。修改find循环部分如下:# 按人类可读顺序遍历本地文件(1→9→10→11)
find "$LOCAL_DIR" -maxdepth 1 -type f | sort -V | while read -r local_file; do
# 新增:过滤非绝对路径或不存在的文件
if [[ ! "$local_file" =~ ^/ || ! -f "$local_file" ]]; then
echo "⚠️ 跳过无效文件(路径异常或不存在):$local_file"
continue
fi
echo "local_file: $local_file"
# 后续逻辑不变...
done
find命令,查看是否有异常路径:find "/home/nick/.wine/drive_c/users/nick/Documents/wechat" -maxdepth 1 -type f
me/nick/...或类似的相对路径,定位具体的异常文件并删除 / 修复。LOCAL_DIR是否存在软链接或路径解析问题:ls -ld "/home/nick/.wine/drive_c/users/nick/Documents/wechat"
boto3、ffmpeg)并配置好 AWS 凭证:import os
import boto3
import subprocess
from botocore.exceptions import ClientError
# 配置参数
MP4_DIR = "mp4" # MP4文件存放目录
MP3_DIR = "mp3" # MP3文件输出目录
S3_BUCKET = "your-bucket-name" # 替换为你的S3桶名
S3_MP4_PREFIX = "videos/" # S3中MP4的存储前缀
S3_MP3_PREFIX = "audios/" # S3中MP3的存储前缀
AUDIO_LINKS_FILE = "audio_links.txt" # MP3链接记录文件
# 确保输出目录存在
os.makedirs(MP3_DIR, exist_ok=True)
# 初始化S3客户端
s3 = boto3.client('s3')
def get_mp3_filename(mp4_filename):
"""从MP4文件名获取对应的MP3文件名"""
return os.path.splitext(mp4_filename)[0] + ".mp3"
def check_local_mp3_exists(mp3_filename):
"""检查本地MP3文件是否存在"""
return os.path.exists(os.path.join(MP3_DIR, mp3_filename))
def convert_mp4_to_mp3(mp4_path, mp3_path):
"""使用ffmpeg将MP4转换为MP3"""
try:
# 提取音频并转换为MP3(保持原采样率,比特率128k)
subprocess.run(
["ffmpeg", "-y", "-i", mp4_path, "-vn", "-acodec", "libmp3lame",
"-ab", "128k", "-ar", "44100", mp3_path],
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
print(f"成功转换: {mp4_path} -> {mp3_path}")
return True
except subprocess.CalledProcessError as e:
print(f"转换失败: {e.stderr}")
return False
def check_s3_file_exists(bucket, key):
"""检查S3中文件是否存在"""
try:
s3.head_object(Bucket=bucket, Key=key)
return True
except ClientError as e:
if e.response['Error']['Code'] == '404':
return False
else:
print(f"S3检查错误: {e}")
return False
def upload_to_s3(local_path, bucket, s3_key):
"""上传文件到S3"""
try:
s3.upload_file(local_path, bucket, s3_key)
print(f"成功上传到S3: {s3_key}")
return True
except Exception as e:
print(f"S3上传失败: {e}")
return False
def get_s3_public_url(bucket, key, region=None):
"""生成S3文件的公共访问URL(需确保文件可公开访问)"""
region = region or boto3.Session().region_name
if region:
return f"https://{bucket}.s3.{region}.amazonaws.com/{key}"
else:
return f"https://{bucket}.s3.amazonaws.com/{key}"
def main():
# 清空现有链接文件(可选,根据需求调整)
open(AUDIO_LINKS_FILE, 'w').close()
# 遍历MP4目录中的所有文件
for mp4_filename in os.listdir(MP4_DIR):
if not mp4_filename.lower().endswith(".mp4"):
continue # 只处理MP4文件
mp4_path = os.path.join(MP4_DIR, mp4_filename)
mp3_filename = get_mp3_filename(mp4_filename)
mp3_path = os.path.join(MP3_DIR, mp3_filename)
# 步骤1: 检查并转换MP4到MP3
if not check_local_mp3_exists(mp3_filename):
print(f"本地MP3不存在,开始转换: {mp3_filename}")
if not convert_mp4_to_mp3(mp4_path, mp3_path):
print(f"跳过文件 {mp4_filename}(转换失败)")
continue
# 步骤2: 检查并上传MP3到S3
s3_mp3_key = f"{S3_MP3_PREFIX}{mp3_filename}"
if not check_s3_file_exists(S3_BUCKET, s3_mp3_key):
print(f"S3中MP3不存在,开始上传: {s3_mp3_key}")
if not upload_to_s3(mp3_path, S3_BUCKET, s3_mp3_key):
print(f"跳过文件 {mp3_filename}(上传失败)")
continue
# 步骤3: 记录MP3的S3链接
mp3_url = get_s3_public_url(S3_BUCKET, s3_mp3_key)
with open(AUDIO_LINKS_FILE, 'a') as f:
f.write(f"{mp3_url}\n")
print(f"已记录链接: {mp3_url}")
print("所有文件处理完成")
if __name__ == "__main__":
main()
pip install boto3
# 同时需要安装ffmpeg并确保在环境变量中(https://ffmpeg.org/download.html)
S3_BUCKET为你的实际 S3 桶名MP4_DIR、MP3_DIR等路径参数S3_MP4_PREFIX和S3_MP3_PREFIXaudio_links.txtaws configure或环境变量)uploader_bug.sh的调试和修改,主要解决了 MP3 目录创建、文件过滤、变量引用、错误处理等问题:#!/bin/bash
set -e
# -------------------------- 1. 配置参数(必改) --------------------------
LOCAL_DIR="/home/nick/.wine/drive_c/users/nick/Documents/wechat" # 本地视频文件夹
S3_BUCKET="www.staroceans.org" # S3桶名
S3_DIR="video/wechat/" # S3目标目录(末尾加/,如"videos/2024/")
# -------------------------- 2. 测试模式开关(选填) --------------------------
DRY_RUN=false # 👉 测试时改 true(只打印不执行),正式上传改 false
# --------------------------------------------------------------------------
# 链接列表文件路径(Dry Run 时也会打印路径,但不实际创建)
LINK_FILE="/home/nick/diabloforum/tools/data/s3_video_links.txt"
MP3_LINK_FILE="/home/nick/diabloforum/tools/data/s3_audio_links.txt"
TOP_FILE="/home/nick/diabloforum/tools/data/top.txt"
BOTTOM_FILE="/home/nick/diabloforum/tools/data/bottom.txt"
VIDEO_FILE="/home/nick/diabloforum/tools/data/video_list.html"
MP3_DIR="${LOCAL_DIR}/mp3" # MP3存放目录单独定义,方便维护
# -------------------------- 核心函数:安全执行命令 --------------------------
safe_exec() {
local cmd="$*"
if [ "$DRY_RUN" = true ]; then
echo "[DRY RUN] 即将执行:$cmd"
else
echo "[实际执行] $cmd"
eval "$cmd" || { echo "命令执行失败: $cmd" >&2; exit 1; } # 增加错误退出
fi
}
echo "========================================"
if [ "$DRY_RUN" = true ]; then
echo "🔴 当前为 DRY RUN 测试模式,不会实际上传/写文件!"
else
echo "🟢 当前为正式上传模式,会执行上传和写链接文件!"
# 初始化链接文件(清空并写入开始标签)
safe_exec "> $LINK_FILE"
safe_exec "echo '<ol>' > $LINK_FILE"
safe_exec "> $MP3_LINK_FILE"
safe_exec "echo '<ol>' > $MP3_LINK_FILE"
# 确保MP3目录存在
safe_exec "mkdir -p \"$MP3_DIR\""
fi
echo "本地目录:$LOCAL_DIR"
echo "MP3转换目录:$MP3_DIR"
echo "S3目标:s3://${S3_BUCKET}/${S3_DIR}"
echo "视频链接文件:$LINK_FILE"
echo "音频链接文件:$MP3_LINK_FILE"
echo "========================================"
# 1. 只处理MP4文件(避免非视频文件导致转换错误),按人类可读顺序遍历
find "$LOCAL_DIR" -maxdepth 1 -type f -name "*.mp4" | sort -V | while read -r local_file; do
echo "local_file: $local_file"
filename=$(basename "$local_file")
echo "filename: $filename"
# 处理MP3文件名(带引号避免空格问题)
mp3_name="${filename%.mp4}.mp3" # 用%.mp4更安全(只去掉最后一个.mp4)
echo "mp3_name: $mp3_name"
mp3_filename="${MP3_DIR}/${mp3_name}"
echo "mp3 filename: $mp3_filename"
# S3路径(带引号处理特殊字符)
s3_path="s3://${S3_BUCKET}/${S3_DIR}${filename}"
s3_mp3_path="s3://${S3_BUCKET}/${S3_DIR}mp3/${mp3_name}"
s3_url="http://${S3_BUCKET}/${S3_DIR}${filename}"
s3_mp3_url="http://${S3_BUCKET}/${S3_DIR}mp3/${mp3_name}" # 补全引号
echo -e "\n=== 处理文件:$filename ==="
# 检查并生成MP3(带引号,保留ffmpeg输出便于调试)
if [ -f "$mp3_filename" ]; then # 更简洁的文件存在判断
echo "MP3文件已存在:$mp3_filename"
else
echo "开始转换MP3:$local_file -> $mp3_filename"
safe_exec "ffmpeg -i \"$local_file\" \"$mp3_filename\"" # 去掉输出重定向,方便排查转换错误
fi
# 检查S3文件是否存在
echo "[步骤1] 检查S3视频文件:$s3_path"
if [ "$DRY_RUN" = true ]; then
s3_exists=false # 测试模式手动控制
s3_mp3_exists=false
else
# 实际检查S3(带引号)
if s3cmd info "$s3_path" >/dev/null 2>&1; then
s3_exists=true
else
s3_exists=false
fi
if s3cmd info "$s3_mp3_path" >/dev/null 2>&1; then
s3_mp3_exists=true
else
s3_mp3_exists=false
fi
fi
# MD5校验逻辑(保持原有逻辑,补充引号)
s3_md5_check=false
if [ "$s3_exists" = true ] && [ "$s3_md5_check" = true ]; then
echo "[结果] S3已存在视频文件,对比MD5"
local_md5=$(md5sum "$local_file" | awk '{print $1}')
if [ "$DRY_RUN" = true ]; then
s3_md5="模拟S3的MD5"
else
s3_md5=$(s3cmd info "$s3_path" | grep "MD5 sum" | awk '{print $3}')
fi
echo "[对比] 本地MD5:$local_md5 | S3 MD5:$s3_md5"
if [ "$local_md5" != "$s3_md5" ]; then
echo "[结论] MD5不一致,需要重新上传"
s3_exists=false
fi
fi
# 上传视频文件
if [ "$s3_exists" = false ]; then
echo "[结果] S3不存在视频文件,准备上传"
safe_exec "s3cmd put --mime-type=\"video/mp4\" --acl-public --continue --check-md5 \"$local_file\" \"$s3_path\""
fi
# 上传MP3文件
if [ "$s3_mp3_exists" = false ]; then
echo "[结果] S3不存在MP3文件,准备上传"
safe_exec "s3cmd put --mime-type=\"audio/mp3\" --acl-public --continue \"$mp3_filename\" \"$s3_mp3_path\""
fi
# 写入链接(带引号,确保时间戳正确获取)
timestamp=$(stat --format="%y" "$local_file" | cut -d. -f1)
safe_exec "echo '<li><a href=\"$s3_url\">$filename ($timestamp)</a></li>' >> \"$LINK_FILE\""
safe_exec "echo '<li><a href=\"$s3_mp3_url\">$mp3_name ($timestamp)</a></li>' >> \"$MP3_LINK_FILE\""
echo "------------------------"
done
# 完善链接列表并生成HTML(补充引号)
safe_exec "echo '</ol>' >> \"$LINK_FILE\""
safe_exec "echo '</ol>' >> \"$MP3_LINK_FILE\""
safe_exec "cat \"$TOP_FILE\" > \"$VIDEO_FILE\""
safe_exec "cat \"$LINK_FILE\" >> \"$VIDEO_FILE\""
safe_exec "echo '<br><hr><br>' >> \"$VIDEO_FILE\""
safe_exec "cat \"$MP3_LINK_FILE\" >> \"$VIDEO_FILE\""
safe_exec "cat \"$BOTTOM_FILE\" >> \"$VIDEO_FILE\""
safe_exec "s3cmd put --mime-type=\"text/html\" --acl-public --continue --check-md5 \"$VIDEO_FILE\" \"s3://${S3_BUCKET}/tools/data/video_list.html\""
echo -e "\n🎉 所有文件处理完成!"
if [ "$DRY_RUN" = true ]; then
echo "🔴 注意:DRY RUN 模式未实际执行任何修改,可核对上述步骤是否符合预期!"
else
echo "🟢 正式模式已完成,链接列表保存在:$LINK_FILE 和 $MP3_LINK_FILE"
fi
mkdir -p "$MP3_DIR"确保转换 MP3 时目录存在,避免因目录不存在导致的 ffmpeg 失败。find命令中添加-name "*.mp4"过滤,避免非视频文件(如 txt、jpg)被误处理。"$local_file"),解决文件名含空格、特殊字符时的解析错误。safe_exec函数增加命令执行失败检查(|| { echo ...; exit 1; }),避免错误被忽略。ffmpeg命令的> /dev/null 2>&1,方便调试转换失败的原因(如编码问题)。${filename%.mp4}.mp3替代${filename%%.mp4}.mp3,避免文件名含多个.mp4后缀时的错误(如file.mp4.copy.mp4)。MP3_DIR变量,调整输出提示信息,让日志更清晰。DRY_RUN=true测试,观察输出的命令是否符合预期,再逐步排查具体错误点。以下是对uploader_bug.sh的调试和修改,主要解决了 MP3 目录创建、文件过滤、变量引用、错误处理等问题:#!/bin/bash
set -e
# -------------------------- 1. 配置参数(必改) --------------------------
LOCAL_DIR="/home/nick/.wine/drive_c/users/nick/Documents/wechat" # 本地视频文件夹
S3_BUCKET="www.staroceans.org" # S3桶名
S3_DIR="video/wechat/" # S3目标目录(末尾加/,如"videos/2024/")
# -------------------------- 2. 测试模式开关(选填) --------------------------
DRY_RUN=false # 👉 测试时改 true(只打印不执行),正式上传改 false
# --------------------------------------------------------------------------
# 链接列表文件路径(Dry Run 时也会打印路径,但不实际创建)
LINK_FILE="/home/nick/diabloforum/tools/data/s3_video_links.txt"
MP3_LINK_FILE="/home/nick/diabloforum/tools/data/s3_audio_links.txt"
TOP_FILE="/home/nick/diabloforum/tools/data/top.txt"
BOTTOM_FILE="/home/nick/diabloforum/tools/data/bottom.txt"
VIDEO_FILE="/home/nick/diabloforum/tools/data/video_list.html"
MP3_DIR="${LOCAL_DIR}/mp3" # MP3存放目录单独定义,方便维护
# -------------------------- 核心函数:安全执行命令 --------------------------
safe_exec() {
local cmd="$*"
if [ "$DRY_RUN" = true ]; then
echo "[DRY RUN] 即将执行:$cmd"
else
echo "[实际执行] $cmd"
eval "$cmd" || { echo "命令执行失败: $cmd" >&2; exit 1; } # 增加错误退出
fi
}
echo "========================================"
if [ "$DRY_RUN" = true ]; then
echo "🔴 当前为 DRY RUN 测试模式,不会实际上传/写文件!"
else
echo "🟢 当前为正式上传模式,会执行上传和写链接文件!"
# 初始化链接文件(清空并写入开始标签)
safe_exec "> $LINK_FILE"
safe_exec "echo '<ol>' > $LINK_FILE"
safe_exec "> $MP3_LINK_FILE"
safe_exec "echo '<ol>' > $MP3_LINK_FILE"
# 确保MP3目录存在
safe_exec "mkdir -p \"$MP3_DIR\""
fi
echo "本地目录:$LOCAL_DIR"
echo "MP3转换目录:$MP3_DIR"
echo "S3目标:s3://${S3_BUCKET}/${S3_DIR}"
echo "视频链接文件:$LINK_FILE"
echo "音频链接文件:$MP3_LINK_FILE"
echo "========================================"
# 1. 只处理MP4文件(避免非视频文件导致转换错误),按人类可读顺序遍历
find "$LOCAL_DIR" -maxdepth 1 -type f -name "*.mp4" | sort -V | while read -r local_file; do
echo "local_file: $local_file"
filename=$(basename "$local_file")
echo "filename: $filename"
# 处理MP3文件名(带引号避免空格问题)
mp3_name="${filename%.mp4}.mp3" # 用%.mp4更安全(只去掉最后一个.mp4)
echo "mp3_name: $mp3_name"
mp3_filename="${MP3_DIR}/${mp3_name}"
echo "mp3 filename: $mp3_filename"
# S3路径(带引号处理特殊字符)
s3_path="s3://${S3_BUCKET}/${S3_DIR}${filename}"
s3_mp3_path="s3://${S3_BUCKET}/${S3_DIR}mp3/${mp3_name}"
s3_url="http://${S3_BUCKET}/${S3_DIR}${filename}"
s3_mp3_url="http://${S3_BUCKET}/${S3_DIR}mp3/${mp3_name}" # 补全引号
echo -e "\n=== 处理文件:$filename ==="
# 检查并生成MP3(带引号,保留ffmpeg输出便于调试)
if [ -f "$mp3_filename" ]; then # 更简洁的文件存在判断
echo "MP3文件已存在:$mp3_filename"
else
echo "开始转换MP3:$local_file -> $mp3_filename"
safe_exec "ffmpeg -i \"$local_file\" \"$mp3_filename\"" # 去掉输出重定向,方便排查转换错误
fi
# 检查S3文件是否存在
echo "[步骤1] 检查S3视频文件:$s3_path"
if [ "$DRY_RUN" = true ]; then
s3_exists=false # 测试模式手动控制
s3_mp3_exists=false
else
# 实际检查S3(带引号)
if s3cmd info "$s3_path" >/dev/null 2>&1; then
s3_exists=true
else
s3_exists=false
fi
if s3cmd info "$s3_mp3_path" >/dev/null 2>&1; then
s3_mp3_exists=true
else
s3_mp3_exists=false
fi
fi
# MD5校验逻辑(保持原有逻辑,补充引号)
s3_md5_check=false
if [ "$s3_exists" = true ] && [ "$s3_md5_check" = true ]; then
echo "[结果] S3已存在视频文件,对比MD5"
local_md5=$(md5sum "$local_file" | awk '{print $1}')
if [ "$DRY_RUN" = true ]; then
s3_md5="模拟S3的MD5"
else
s3_md5=$(s3cmd info "$s3_path" | grep "MD5 sum" | awk '{print $3}')
fi
echo "[对比] 本地MD5:$local_md5 | S3 MD5:$s3_md5"
if [ "$local_md5" != "$s3_md5" ]; then
echo "[结论] MD5不一致,需要重新上传"
s3_exists=false
fi
fi
# 上传视频文件
if [ "$s3_exists" = false ]; then
echo "[结果] S3不存在视频文件,准备上传"
safe_exec "s3cmd put --mime-type=\"video/mp4\" --acl-public --continue --check-md5 \"$local_file\" \"$s3_path\""
fi
# 上传MP3文件
if [ "$s3_mp3_exists" = false ]; then
echo "[结果] S3不存在MP3文件,准备上传"
safe_exec "s3cmd put --mime-type=\"audio/mp3\" --acl-public --continue \"$mp3_filename\" \"$s3_mp3_path\""
fi
# 写入链接(带引号,确保时间戳正确获取)
timestamp=$(stat --format="%y" "$local_file" | cut -d. -f1)
safe_exec "echo '<li><a href=\"$s3_url\">$filename ($timestamp)</a></li>' >> \"$LINK_FILE\""
safe_exec "echo '<li><a href=\"$s3_mp3_url\">$mp3_name ($timestamp)</a></li>' >> \"$MP3_LINK_FILE\""
echo "------------------------"
done
# 完善链接列表并生成HTML(补充引号)
safe_exec "echo '</ol>' >> \"$LINK_FILE\""
safe_exec "echo '</ol>' >> \"$MP3_LINK_FILE\""
safe_exec "cat \"$TOP_FILE\" > \"$VIDEO_FILE\""
safe_exec "cat \"$LINK_FILE\" >> \"$VIDEO_FILE\""
safe_exec "echo '<br><hr><br>' >> \"$VIDEO_FILE\""
safe_exec "cat \"$MP3_LINK_FILE\" >> \"$VIDEO_FILE\""
safe_exec "cat \"$BOTTOM_FILE\" >> \"$VIDEO_FILE\""
safe_exec "s3cmd put --mime-type=\"text/html\" --acl-public --continue --check-md5 \"$VIDEO_FILE\" \"s3://${S3_BUCKET}/tools/data/video_list.html\""
echo -e "\n🎉 所有文件处理完成!"
if [ "$DRY_RUN" = true ]; then
echo "🔴 注意:DRY RUN 模式未实际执行任何修改,可核对上述步骤是否符合预期!"
else
echo "🟢 正式模式已完成,链接列表保存在:$LINK_FILE 和 $MP3_LINK_FILE"
fi
mkdir -p "$MP3_DIR"确保转换 MP3 时目录存在,避免因目录不存在导致的 ffmpeg 失败。find命令中添加-name "*.mp4"过滤,避免非视频文件(如 txt、jpg)被误处理。"$local_file"),解决文件名含空格、特殊字符时的解析错误。safe_exec函数增加命令执行失败检查(|| { echo ...; exit 1; }),避免错误被忽略。ffmpeg命令的> /dev/null 2>&1,方便调试转换失败的原因(如编码问题)。${filename%.mp4}.mp3替代${filename%%.mp4}.mp3,避免文件名含多个.mp4后缀时的错误(如file.mp4.copy.mp4)。MP3_DIR变量,调整输出提示信息,让日志更清晰。DRY_RUN=true测试,观察输出的命令是否符合预期,再逐步排查具体错误点。./uploader.sh: line 1: #!/bin/bash: No such file or directory
UTF-8 BOM头(字节顺序标记),导致系统无法正确识别#!/bin/bash解释器路径。解决方法:去除文件的BOM头,执行以下命令:# 使用sed移除BOM头
sed -i '1s/^\xef\xbb\xbf//' uploader.sh
2084_4.mp4时出现:local_file: me/nick/.wine/drive_c/users/nick/Documents/wechat/2084_4.mp4
Error opening input file me/nick/.../2084_4.mp4: No such file or directory
me/nick/...(正确路径应为/home/nick/...),可能是文件名包含特殊字符(如换行、不可见字符)或文件实际不存在导致find命令返回异常路径。解决方法:LOCAL_DIR目录:ls -l /home/nick/.wine/drive_c/users/nick/Documents/wechat/2084_4.mp4
while read -r local_file; do循环内第一行添加:# 检查文件是否存在,不存在则跳过
if [ ! -f "$local_file" ]; then
echo "⚠️ 警告:文件不存在,跳过处理:$local_file"
continue
fi
#!/bin/bash
set -e
# ...(其他配置参数不变)...
# 1. 按人类可读顺序遍历本地文件(1→9→10→11)
find "$LOCAL_DIR" -maxdepth 1 -type f | sort -V | while read -r local_file; do
# 新增:检查文件是否存在,避免无效路径
if [ ! -f "$local_file" ]; then
echo "⚠️ 警告:文件不存在,跳过处理:$local_file"
continue
fi
echo "local_file: $local_file"
filename=$(basename "$local_file")
echo "filename: $filename"
mp3_name=${filename%%.mp4}.mp3
echo " mp3_name: $mp3_name;"
mp3_filename=${LOCAL_DIR}/mp3/$mp3_name
echo "mp3 filename: $mp3_filename;"
# ...(后续逻辑不变)...
done
# ...(其他部分不变)...
sed -i '1s/^\xef\xbb\xbf//' uploader.sh
2084_4.mp4文件是否存在,若存在且路径正确,重新执行脚本即可。local_file的路径会被截断,比如最后变成了/nick/.wine/...而不是正确的/home/nick/.wine/...,导致后续的ffmpeg命令失败。首先,我需要分析可能导致路径截断的原因。find "$LOCAL_DIR" -maxdepth 1 -type f | sort -V | while read -r local_file; do ...来遍历文件。LOCAL_DIR是/home/nick/.wine/drive_c/users/nick/Documents/wechat,理论上find命令应该输出以这个路径开头的文件。read命令在读取时出现问题。虽然用户提到文件存在,但路径解析可能出错。|导致的子 shell 问题。在while read循环中,由于管道的存在,循环可能在子 shell 中运行,某些变量或环境可能受到影响,但这里主要是路径读取,可能不太相关。sort -V命令的问题,比如在排序过程中意外修改了路径。不过sort -V通常是按版本号排序,不会修改路径本身。set -e,这会让脚本在任何命令失败时退出,但用户的脚本并没有退出,而是继续处理,直到遇到错误的路径,这说明可能中间有命令虽然失败但没有触发set -e,不过这可能和路径截断无关。HUD共振2.mp4时,local_file变成了/nick/...,缺少了home/部分。这可能是因为find命令输出的路径被截断了,或者read命令读取时出现了错误。find命令的输出是否正确。可以在脚本中临时添加find "$LOCAL_DIR" -maxdepth 1 -type f | sort -V > /tmp/find_output.txt,然后查看输出文件中的路径是否完整,确认是find的问题还是后续处理的问题。while循环在子 shell 中运行。使用进程替换而不是管道,比如while read -r local_file; do ... done < <(find "$LOCAL_DIR" -maxdepth 1 -type f | sort -V),这样循环在当前 shell 中运行,可能避免一些子 shell 导致的问题。read命令默认会将反斜杠作为转义字符,可能导致路径被错误解析。可以使用IFS= read -r local_file来禁用字段分隔符,避免空格等导致的截断,同时保留反斜杠。LOCAL_DIR是否有拼写错误,或者在脚本运行过程中被意外修改。不过用户前面的文件处理正确,说明LOCAL_DIR初始是对的,但可能在循环中被修改?脚本中没有修改LOCAL_DIR的地方,所以可能性低。read命令在处理包含特殊字符的路径时出现问题,或者管道导致的子 shell 影响。因此,修改while循环的写法,使用进程替换,并正确设置IFS可能解决问题。find "$LOCAL_DIR" -maxdepth 1 -type f | sort -V | while read -r local_file; dowhile IFS= read -r local_file; dodone < <(find "$LOCAL_DIR" -maxdepth 1 -type f | sort -V)IFS=确保read不会忽略前导 / 尾随空格,-r避免反斜杠转义。local_file的检查,比如如果路径不以/home/nick/开头,就报错并跳过,避免后续命令失败,比如:if [[ ! "$local_file" == "/home/nick/"* ]]; then echo "错误:路径异常 $local_file" >&2 continuefiffmpeg和s3cmd是否有输出干扰find的管道,比如这两个命令可能输出到 stdout,混入find的输出中,但脚本中ffmpeg的输出被重定向到/dev/null,s3cmd在正式模式下的输出可能不会混入,因为是在循环内部执行,而find的输出是在循环开始前生成的,所以可能性较低,但可以确认find的输出是否纯净。while循环的输入方式,使用进程替换和正确的read参数,以及添加路径校验。local_file路径被截断(丢失home/部分)很可能是由于管道子shell环境下的变量/路径解析异常,或read命令处理特殊文件名时的行为导致的。结合脚本逻辑,可按以下方式修复:find ... | sort -V | while read -r local_file的管道方式遍历文件,这种结构会导致while循环在子shell中执行。当处理大量文件或包含特殊字符(如中文、空格、特殊符号)的文件名时,可能出现:read命令默认会忽略前导/尾随空格,或对反斜杠转义,导致路径截断< <(...)),让while循环在当前shell执行,避免子shell环境问题。read命令的安全性IFS= read -r禁用字段分隔符,确保路径中的空格、特殊字符被正确读取。# 替换原有的find管道遍历部分
# 1. 按人类可读顺序遍历本地文件(修复路径截断问题)
while IFS= read -r local_file; do
# 路径校验:确保路径以正确的根目录开头
if [[ ! "$local_file" == "${LOCAL_DIR}"* ]]; then
echo "⚠️ 警告:检测到异常路径 $local_file,已跳过" >&2
continue
fi
echo "local_file: $local_file"
filename=$(basename "$local_file")
# 后续处理逻辑保持不变...
# ...(省略原有代码)
done < <(find "$LOCAL_DIR" -maxdepth 1 -type f | sort -V)
find ... | sort -V的输出通过< <(...)传递给while循环,避免子shell创建,确保路径变量在当前环境中解析。IFS= read -r local_file确保:IFS=:禁用默认的字段分隔符(空格、制表符等),防止路径中的空格导致截断-r:禁止read对反斜杠\进行转义,避免路径中特殊字符被误解析[[ ! "$local_file" == "${LOCAL_DIR}"* ]]检查路径是否以LOCAL_DIR开头,一旦发现异常(如丢失home/)立即跳过,避免后续ffmpeg等命令因错误路径失败。LOCAL_DIR目录下是否存在包含特殊字符(如换行符、控制字符)的文件,可通过ls -la "$LOCAL_DIR"查看,如有异常文件名建议重命名。find "$LOCAL_DIR" -maxdepth 1 -type f | sort -V > /tmp/file_list.txt,运行后检查/tmp/file_list.txt中的路径是否完整,确认是否是find命令本身输出异常。find "$LOCAL_DIR" -maxdepth 1 -type f | sort -V | while read -r local_file; do。这里 LOCAL_DIR 是 “/home/nick/.wine/drive_c/users/nick/Documents/wechat”,正常情况下 find 应该输出以该路径开头的完整路径。如果出现 “ome/...”,可能是 find 命令的输出被错误处理了,比如有换行符或者特殊字符导致路径被截断。read -r local_file,并且变量引用有双引号。[ -f "$local_file" ],如果文件不存在,则跳过并提示。filename=$(basename "$local_file")之前,添加:# 1. 按人类可读顺序遍历本地文件(1→9→10→11)
find "$LOCAL_DIR" -maxdepth 1 -type f | sort -V | while read -r local_file; do
# 新增:检查文件路径是否有效(文件是否存在)
if [ ! -f "$local_file" ]; then
echo "⚠️ 警告:检测到无效文件路径 $local_file,已跳过"
continue
fi
filename=$(basename "$local_file")
s3_path="s3://${S3_BUCKET}/${S3_DIR}${filename}"
s3_url="http://${S3_BUCKET}/${S3_DIR}${filename}"
echo -e "\n=== 处理文件:$filename ==="
# 后续处理逻辑...
done
local_file后,首先通过[ ! -f "$local_file" ]检查该路径是否为真实存在的文件:continue跳过后续处理import os
import subprocess
import boto3
from botocore.exceptions import BotoCoreError, ClientError
import logging
# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# S3配置
S3_BUCKET = "your-bucket-name"
S3_REGION = "your-region"
# 初始化S3客户端
s3_client = boto3.client('s3', region_name=S3_REGION)
def check_mp3_file(file_path):
"""检查文件是否为有效的MP3文件"""
if not os.path.exists(file_path):
logger.error(f"文件不存在: {file_path}")
return False
if not file_path.lower().endswith('.mp3'):
logger.error(f"不是MP3文件: {file_path}")
return False
# 使用ffmpeg检查文件完整性
try:
result = subprocess.run(
['ffmpeg', '-v', 'error', '-i', file_path, '-f', 'null', '-'],
capture_output=True,
text=True
)
if result.returncode != 0:
logger.error(f"MP3文件损坏,ffmpeg错误: {result.stderr}")
return False
logger.info(f"MP3文件检查通过: {file_path}")
return True
except Exception as e:
logger.error(f"检查MP3文件时出错: {str(e)}")
return False
def process_mp3_with_ffmpeg(input_path, output_path, bitrate="128k"):
"""使用ffmpeg处理MP3文件(可用于标准化编码参数)"""
try:
# ffmpeg命令:保持音频编码,调整比特率(或根据需要修改参数)
cmd = [
'ffmpeg', '-y', # 覆盖输出文件
'-i', input_path,
'-c:a', 'libmp3lame', # 使用MP3编码器
'-b:a', bitrate, # 设置比特率
'-vn', # 无视频流
output_path
]
result = subprocess.run(
cmd,
capture_output=True,
text=True
)
if result.returncode != 0:
logger.error(f"ffmpeg处理失败: {result.stderr}")
return False
logger.info(f"MP3处理完成,输出至: {output_path}")
return True
except Exception as e:
logger.error(f"ffmpeg处理时出错: {str(e)}")
return False
def upload_to_s3(local_path, s3_key):
"""上传文件到S3"""
try:
s3_client.upload_file(local_path, S3_BUCKET, s3_key)
logger.info(f"成功上传至S3: s3://{S3_BUCKET}/{s3_key}")
return True
except (BotoCoreError, ClientError) as e:
logger.error(f"S3上传失败: {str(e)}")
return False
except Exception as e:
logger.error(f"上传过程出错: {str(e)}")
return False
def process_and_upload_mp3(input_mp3, s3_dest_key, temp_output="temp_processed.mp3"):
"""完整流程:检查->处理->上传"""
# 1. 检查MP3文件
if not check_mp3_file(input_mp3):
return False
# 2. 使用ffmpeg处理
if not process_mp3_with_ffmpeg(input_mp3, temp_output):
return False
# 3. 上传到S3
success = upload_to_s3(temp_output, s3_dest_key)
# 4. 清理临时文件
if os.path.exists(temp_output):
os.remove(temp_output)
logger.info("临时文件已清理")
return success
# 使用示例
if __name__ == "__main__":
input_file = "input.mp3" # 本地输入MP3文件
s3_key = "audio/processed.mp3" # S3中的目标路径
# 执行处理和上传
process_and_upload_mp3(input_file, s3_key)
pip install boto3S3_BUCKET和S3_REGION为实际值process_mp3_with_ffmpeg函数中修改ffmpeg参数(如比特率、采样率等)uploader.sh 脚本修改的MP3处理版本,保留原有框架和逻辑,仅针对MP3文件特性调整关键参数:#!/bin/bash
set -e
# -------------------------- 1. 配置参数(必改) --------------------------
LOCAL_DIR="/home/nick/.wine/drive_c/users/nick/Documents/wechat_audio" # 本地MP3文件夹(修改为你的MP3目录)
S3_BUCKET="www.staroceans.org" # S3桶名(保持不变)
S3_DIR="audio/wechat/" # S3目标目录(改为音频目录,末尾加/)
# -------------------------- 2. 测试模式开关(选填) --------------------------
DRY_RUN=true # 👉 测试时改 true(只打印不执行),正式上传改 false
# --------------------------------------------------------------------------
# 链接列表文件路径(保持原有逻辑,区分视频和音频列表)
LINK_FILE="/home/nick/diabloforum/tools/data/s3_audio_links.txt" # 改为音频链接文件
TOP_FILE="/home/nick/diabloforum/tools/data/top.txt" # 可复用或单独准备音频的top文件
BOTTOM_FILE="/home/nick/diabloforum/tools/data/bottom.txt" # 同上
AUDIO_FILE="/home/nick/diabloforum/tools/data/audio_list.html" # 音频列表HTML文件
# -------------------------- 核心函数:安全执行命令(保持不变) --------------------------
safe_exec() {
local cmd="$*"
if [ "$DRY_RUN" = true ]; then
echo "[DRY RUN] 即将执行:$cmd"
else
echo "[实际执行] $cmd"
eval "$cmd"
fi
}
echo "========================================"
if [ "$DRY_RUN" = true ]; then
echo "🔴 当前为 DRY RUN 测试模式,不会实际上传/写文件!"
else
echo "🟢 当前为正式上传模式,会执行上传和写链接文件!"
safe_exec "> $LINK_FILE" # 清空旧音频链接文件
safe_exec "echo '<ol>' > $LINK_FILE"
fi
echo "本地MP3目录:$LOCAL_DIR"
echo "S3目标:s3://${S3_BUCKET}/${S3_DIR}"
echo "音频链接列表文件:$LINK_FILE"
echo "========================================"
# 1. 只遍历MP3文件(按人类可读顺序)
find "$LOCAL_DIR" -maxdepth 1 -type f -name "*.mp3" | sort -V | while read -r local_file; do # 增加 -name "*.mp3" 过滤
filename=$(basename "$local_file")
s3_path="s3://${S3_BUCKET}/${S3_DIR}${filename}"
s3_url="http://${S3_BUCKET}/${S3_DIR}${filename}" # 保持URL格式一致
echo -e "\n=== 处理MP3文件:$filename ==="
# 2. 检查S3文件是否存在(逻辑不变)
echo "[步骤1] 检查S3是否存在文件:$s3_path"
if [ "$DRY_RUN" = true ]; then
s3cmd info "$s3_path"
s3_exists=false # 测试模式:手动切换true/false模拟场景
else
if s3cmd info "$s3_path" >/dev/null 2>&1; then
s3_exists=true
else
s3_exists=false
fi
fi
s3_md5_check=false # 保持MD5检查开关
# 3. 存在性+MD5检查逻辑(不变)
if [ "$s3_exists" = true ] && [ "$s3_md5_check" = true ]; then
echo "[结果] S3已存在该文件,准备对比MD5完整性"
local_md5=$(md5sum "$local_file" | awk '{print $1}')
if [ "$DRY_RUN" = true ]; then
s3_md5="模拟S3文件的MD5值"
else
s3_md5=$(s3cmd info "$s3_path" | grep "MD5 sum" | awk '{print $3}')
fi
echo "[对比] 本地MD5:$local_md5 | S3 MD5:$s3_md5"
if [ "$local_md5" = "$s3_md5" ] && [ "$DRY_RUN" = false ]; then
echo "[结论] MD5一致,跳过上传"
s3_exists=true
elif [ "$local_md5" != "$s3_md5" ]; then
echo "[结论] MD5不一致,需要重新上传"
s3_exists=false
else
echo "[DRY RUN] 模拟MD5一致,跳过上传并写链接"
fi
fi
# 4. 上传逻辑(仅修改MIME类型为MP3专用)
if [ "$s3_exists" = false ]; then
echo "[结果] S3不存在该文件,准备上传"
# MP3的MIME类型为 audio/mpeg(原视频是 video/mp4)
safe_exec "s3cmd put --mime-type=\"audio/mpeg\" --acl-public --continue --check-md5 '$local_file' '$s3_path'"
fi
# 5. 生成音频链接(逻辑不变,路径指向音频列表)
timestamp=$(stat --format="%y" "$local_file" | cut -d. -f1)
safe_exec "echo '<li><a href=\"$s3_url\">$filename ($timestamp)</a></li>' >> $LINK_FILE"
echo "------------------------"
done
# 6. 生成音频列表HTML(对应音频文件)
safe_exec "echo '</ol>' >> $LINK_FILE"
safe_exec "cat $TOP_FILE > $AUDIO_FILE"
safe_exec "cat $LINK_FILE >> $AUDIO_FILE"
safe_exec "cat $BOTTOM_FILE >> $AUDIO_FILE"
# 上传音频列表HTML到S3(路径可自定义)
safe_exec "s3cmd put --mime-type=\"text/html\" --acl-public --continue --check-md5 '$AUDIO_FILE' 's3://${S3_BUCKET}/tools/data/audio_list.html'"
echo -e "\n🎉 所有MP3文件处理完成!"
if [ "$DRY_RUN" = true ]; then
echo "🔴 注意:DRY RUN 模式未实际执行任何修改,可核对上述步骤是否符合预期!"
else
echo "🟢 正式模式已完成,音频链接列表保存在:$LINK_FILE"
fi
LOCAL_DIR 改为MP3文件所在目录S3_DIR 改为音频存储目录(如 audio/wechat/)s3_audio_links.txt、audio_list.html)find 命令增加 -name "*.mp3" 只处理MP3文件--mime-type 改为 audio/mpeg(MP3标准MIME类型)s3cmd 已正确配置(可通过 s3cmd ls 测试)top.txt/bottom.txt,可单独创建音频专用版本并修改路径DRY_RUN=true 测试,确认无误后改 false 正式执行。以下是基于你的 uploader.sh 脚本修改的MP3处理版本,保留原有框架和逻辑,仅针对MP3文件特性调整关键参数:#!/bin/bash
set -e
# -------------------------- 1. 配置参数(必改) --------------------------
LOCAL_DIR="/home/nick/.wine/drive_c/users/nick/Documents/wechat_audio" # 本地MP3文件夹(修改为你的MP3目录)
S3_BUCKET="www.staroceans.org" # S3桶名(保持不变)
S3_DIR="audio/wechat/" # S3目标目录(改为音频目录,末尾加/)
# -------------------------- 2. 测试模式开关(选填) --------------------------
DRY_RUN=true # 👉 测试时改 true(只打印不执行),正式上传改 false
# --------------------------------------------------------------------------
# 链接列表文件路径(保持原有逻辑,区分视频和音频列表)
LINK_FILE="/home/nick/diabloforum/tools/data/s3_audio_links.txt" # 改为音频链接文件
TOP_FILE="/home/nick/diabloforum/tools/data/top.txt" # 可复用或单独准备音频的top文件
BOTTOM_FILE="/home/nick/diabloforum/tools/data/bottom.txt" # 同上
AUDIO_FILE="/home/nick/diabloforum/tools/data/audio_list.html" # 音频列表HTML文件
# -------------------------- 核心函数:安全执行命令(保持不变) --------------------------
safe_exec() {
local cmd="$*"
if [ "$DRY_RUN" = true ]; then
echo "[DRY RUN] 即将执行:$cmd"
else
echo "[实际执行] $cmd"
eval "$cmd"
fi
}
echo "========================================"
if [ "$DRY_RUN" = true ]; then
echo "🔴 当前为 DRY RUN 测试模式,不会实际上传/写文件!"
else
echo "🟢 当前为正式上传模式,会执行上传和写链接文件!"
safe_exec "> $LINK_FILE" # 清空旧音频链接文件
safe_exec "echo '<ol>' > $LINK_FILE"
fi
echo "本地MP3目录:$LOCAL_DIR"
echo "S3目标:s3://${S3_BUCKET}/${S3_DIR}"
echo "音频链接列表文件:$LINK_FILE"
echo "========================================"
# 1. 只遍历MP3文件(按人类可读顺序)
find "$LOCAL_DIR" -maxdepth 1 -type f -name "*.mp3" | sort -V | while read -r local_file; do # 增加 -name "*.mp3" 过滤
filename=$(basename "$local_file")
s3_path="s3://${S3_BUCKET}/${S3_DIR}${filename}"
s3_url="http://${S3_BUCKET}/${S3_DIR}${filename}" # 保持URL格式一致
echo -e "\n=== 处理MP3文件:$filename ==="
# 2. 检查S3文件是否存在(逻辑不变)
echo "[步骤1] 检查S3是否存在文件:$s3_path"
if [ "$DRY_RUN" = true ]; then
s3cmd info "$s3_path"
s3_exists=false # 测试模式:手动切换true/false模拟场景
else
if s3cmd info "$s3_path" >/dev/null 2>&1; then
s3_exists=true
else
s3_exists=false
fi
fi
s3_md5_check=false # 保持MD5检查开关
# 3. 存在性+MD5检查逻辑(不变)
if [ "$s3_exists" = true ] && [ "$s3_md5_check" = true ]; then
echo "[结果] S3已存在该文件,准备对比MD5完整性"
local_md5=$(md5sum "$local_file" | awk '{print $1}')
if [ "$DRY_RUN" = true ]; then
s3_md5="模拟S3文件的MD5值"
else
s3_md5=$(s3cmd info "$s3_path" | grep "MD5 sum" | awk '{print $3}')
fi
echo "[对比] 本地MD5:$local_md5 | S3 MD5:$s3_md5"
if [ "$local_md5" = "$s3_md5" ] && [ "$DRY_RUN" = false ]; then
echo "[结论] MD5一致,跳过上传"
s3_exists=true
elif [ "$local_md5" != "$s3_md5" ]; then
echo "[结论] MD5不一致,需要重新上传"
s3_exists=false
else
echo "[DRY RUN] 模拟MD5一致,跳过上传并写链接"
fi
fi
# 4. 上传逻辑(仅修改MIME类型为MP3专用)
if [ "$s3_exists" = false ]; then
echo "[结果] S3不存在该文件,准备上传"
# MP3的MIME类型为 audio/mpeg(原视频是 video/mp4)
safe_exec "s3cmd put --mime-type=\"audio/mpeg\" --acl-public --continue --check-md5 '$local_file' '$s3_path'"
fi
# 5. 生成音频链接(逻辑不变,路径指向音频列表)
timestamp=$(stat --format="%y" "$local_file" | cut -d. -f1)
safe_exec "echo '<li><a href=\"$s3_url\">$filename ($timestamp)</a></li>' >> $LINK_FILE"
echo "------------------------"
done
# 6. 生成音频列表HTML(对应音频文件)
safe_exec "echo '</ol>' >> $LINK_FILE"
safe_exec "cat $TOP_FILE > $AUDIO_FILE"
safe_exec "cat $LINK_FILE >> $AUDIO_FILE"
safe_exec "cat $BOTTOM_FILE >> $AUDIO_FILE"
# 上传音频列表HTML到S3(路径可自定义)
safe_exec "s3cmd put --mime-type=\"text/html\" --acl-public --continue --check-md5 '$AUDIO_FILE' 's3://${S3_BUCKET}/tools/data/audio_list.html'"
echo -e "\n🎉 所有MP3文件处理完成!"
if [ "$DRY_RUN" = true ]; then
echo "🔴 注意:DRY RUN 模式未实际执行任何修改,可核对上述步骤是否符合预期!"
else
echo "🟢 正式模式已完成,音频链接列表保存在:$LINK_FILE"
fi
LOCAL_DIR 改为MP3文件所在目录S3_DIR 改为音频存储目录(如 audio/wechat/)s3_audio_links.txt、audio_list.html)find 命令增加 -name "*.mp3" 只处理MP3文件--mime-type 改为 audio/mpeg(MP3标准MIME类型)s3cmd 已正确配置(可通过 s3cmd ls 测试)top.txt/bottom.txt,可单独创建音频专用版本并修改路径DRY_RUN=true 测试,确认无误后改 false 正式执行。#!/bin/bash
set -e
# -------------------------- 1. 配置参数(必改) --------------------------
LOCAL_DIR="/home/nick/.wine/drive_c/users/nick/Documents/wechat" # 本地视频文件夹
S3_BUCKET="www.staroceans.org" # S3桶名
S3_DIR="video/wechat/" # S3目标目录(末尾加/,如"videos/2024/")
# MP3相关配置(新增)
MP3_LOCAL_SUBDIR="mp3" # 本地MP3子目录名(在LOCAL_DIR下)
MP3_S3_SUBDIR="mp3/" # S3上MP3子目录名(在S3_DIR下)
# -------------------------- 2. 测试模式开关(选填) --------------------------
DRY_RUN=true # 👉 测试时改 true(只打印不执行),正式上传改 false
# --------------------------------------------------------------------------
# 链接列表文件路径(保留原MP4的,新增MP3的)
# MP4相关
LINK_FILE="/home/nick/diabloforum/tools/data/s3_video_links.txt"
TOP_FILE="/home/nick/diabloforum/tools/data/top.txt"
BOTTOM_FILE="/home/nick/diabloforum/tools/data/bottom.txt"
VIDEO_FILE="/home/nick/diabloforum/tools/data/video_list.html"
# MP3相关(新增)
MP3_LINK_FILE="/home/nick/diabloforum/tools/data/s3_audio_links.txt"
MP3_HTML_FILE="/home/nick/diabloforum/tools/data/audio_list.html"
# -------------------------- 核心函数:安全执行命令 --------------------------
safe_exec() {
local cmd="$*"
if [ "$DRY_RUN" = true ]; then
echo "[DRY RUN] 即将执行:$cmd"
else
echo "[实际执行] $cmd"
eval "$cmd"
fi
}
echo "========================================"
if [ "$DRY_RUN" = true ]; then
echo "🔴 当前为 DRY RUN 测试模式,不会实际上传/写文件!"
else
echo "🟢 当前为正式上传模式,会执行上传和写链接文件!"
# 清空原有链接文件(同时处理MP4和MP3)
safe_exec "> $LINK_FILE"
safe_exec "echo '<ol>' > $LINK_FILE"
safe_exec "> $MP3_LINK_FILE"
safe_exec "echo '<ol>' > $MP3_LINK_FILE"
fi
echo "本地目录:$LOCAL_DIR"
echo "本地MP3子目录:$LOCAL_DIR/$MP3_LOCAL_SUBDIR"
echo "S3目标(MP4):s3://${S3_BUCKET}/${S3_DIR}"
echo "S3目标(MP3):s3://${S3_BUCKET}/${S3_DIR}${MP3_S3_SUBDIR}"
echo "MP4链接列表:$LINK_FILE"
echo "MP3链接列表:$MP3_LINK_FILE"
echo "========================================"
# 1. 只处理MP4文件,按人类可读顺序遍历
find "$LOCAL_DIR" -maxdepth 1 -type f -name "*.mp4" | sort -V | while read -r local_mp4; do
filename=$(basename "$local_mp4")
# 提取文件名(不含后缀)用于生成MP3文件名
filename_noext="${filename%.mp4}"
# MP4相关路径
s3_mp4_path="s3://${S3_BUCKET}/${S3_DIR}${filename}"
s3_mp4_url="http://${S3_BUCKET}/${S3_DIR}${filename}"
# MP3相关路径(新增)
local_mp3_dir="$LOCAL_DIR/$MP3_LOCAL_SUBDIR"
local_mp3="$local_mp3_dir/${filename_noext}.mp3"
s3_mp3_path="s3://${S3_BUCKET}/${S3_DIR}${MP3_S3_SUBDIR}${filename_noext}.mp3"
s3_mp3_url="http://${S3_BUCKET}/${S3_DIR}${MP3_S3_SUBDIR}${filename_noext}.mp3"
echo -e "\n=== 处理文件:$filename ==="
# ====================== 原有MP4处理逻辑(保持不变) ======================
echo -e "\n[MP4处理] 开始处理视频文件"
# 检查S3上MP4是否存在
echo "[MP4步骤1] 检查S3是否存在:$s3_mp4_path"
if [ "$DRY_RUN" = true ]; then
s3cmd info "$s3_mp4_path"
s3_mp4_exists=false # 测试模式可手动修改
else
if s3cmd info "$s3_mp4_path" >/dev/null 2>&1; then
s3_mp4_exists=true
else
s3_mp4_exists=false
fi
fi
s3_md5_check=false
# MP4的MD5检查与上传
if [ "$s3_mp4_exists" = true ] && [ "$s3_md5_check" = true ]; then
echo "[MP4步骤2] 对比MD5完整性"
local_mp4_md5=$(md5sum "$local_mp4" | awk '{print $1}')
if [ "$DRY_RUN" = true ]; then
s3_mp4_md5="模拟S3的MP4 MD5"
else
s3_mp4_md5=$(s3cmd info "$s3_mp4_path" | grep "MD5 sum" | awk '{print $3}')
fi
echo "[MP4对比] 本地MD5:$local_mp4_md5 | S3 MD5:$s3_mp4_md5"
if [ "$local_mp4_md5" = "$s3_mp4_md5" ] && [ "$DRY_RUN" = false ]; then
echo "[MP4结论] MD5一致,跳过上传"
s3_mp4_exists=true
else
echo "[MP4结论] MD5不一致,需要重新上传"
s3_mp4_exists=false
fi
fi
if [ "$s3_mp4_exists" = false ]; then
echo "[MP4步骤3] 上传MP4到S3"
safe_exec "s3cmd put --mime-type=\"video/mp4\" --acl-public --continue --check-md5 '$local_mp4' '$s3_mp4_path'"
fi
# 写入MP4链接
mp4_timestamp=$(stat --format="%y" "$local_mp4" | cut -d. -f1)
safe_exec "echo '<li><a href=\"$s3_mp4_url\">$filename ($mp4_timestamp)</a></li>' >> $LINK_FILE"
echo "[MP4处理] 完成"
# ====================== 新增MP3处理逻辑 ======================
echo -e "\n[MP3处理] 开始处理音频文件"
# 1. 确保本地MP3目录存在
safe_exec "mkdir -p $local_mp3_dir"
# 2. 检查本地是否有对应MP3,没有则用ffmpeg生成
echo "[MP3步骤1] 检查本地MP3:$local_mp3"
if [ -f "$local_mp3" ]; then
echo "[MP3结果] 本地已存在MP3文件"
else
echo "[MP3结果] 本地不存在MP3,准备生成"
# 使用ffmpeg从MP4提取音频生成MP3(-vn表示仅音频,-acodec指定编码器)
safe_exec "ffmpeg -y -i '$local_mp4' -vn -acodec libmp3lame '$local_mp3'"
fi
# 3. 检查S3上MP3是否存在
echo "[MP3步骤2] 检查S3是否存在:$s3_mp3_path"
if [ "$DRY_RUN" = true ]; then
s3cmd info "$s3_mp3_path"
s3_mp3_exists=false # 测试模式可手动修改
else
if s3cmd info "$s3_mp3_path" >/dev/null 2>&1; then
s3_mp3_exists=true
else
s3_mp3_exists=false
fi
fi
# 4. MP3的MD5检查与上传
if [ "$s3_mp3_exists" = true ] && [ "$s3_md5_check" = true ]; then
echo "[MP3步骤3] 对比MD5完整性"
local_mp3_md5=$(md5sum "$local_mp3" | awk '{print $1}')
if [ "$DRY_RUN" = true ]; then
s3_mp3_md5="模拟S3的MP3 MD5"
else
s3_mp3_md5=$(s3cmd info "$s3_mp3_path" | grep "MD5 sum" | awk '{print $3}')
fi
echo "[MP3对比] 本地MD5:$local_mp3_md5 | S3 MD5:$s3_mp3_md5"
if [ "$local_mp3_md5" = "$s3_mp3_md5" ] && [ "$DRY_RUN" = false ]; then
echo "[MP3结论] MD5一致,跳过上传"
s3_mp3_exists=true
else
echo "[MP3结论] MD5不一致,需要重新上传"
s3_mp3_exists=false
fi
fi
if [ "$s3_mp3_exists" = false ]; then
echo "[MP3步骤4] 上传MP3到S3"
safe_exec "s3cmd put --mime-type=\"audio/mpeg\" --acl-public --continue --check-md5 '$local_mp3' '$s3_mp3_path'"
fi
# 写入MP3链接
mp3_timestamp=$(stat --format="%y" "$local_mp3" | cut -d. -f1)
safe_exec "echo '<li><a href=\"$s3_mp3_url\">${filename_noext}.mp3 ($mp3_timestamp)</a></li>' >> $MP3_LINK_FILE"
echo "[MP3处理] 完成"
echo "------------------------"
done
# ====================== 生成最终HTML文件(同时处理MP4和MP3) ======================
# 处理MP4列表
safe_exec "echo '</ol>' >> $LINK_FILE"
safe_exec "cat $TOP_FILE > $VIDEO_FILE"
safe_exec "cat $LINK_FILE >> $VIDEO_FILE"
safe_exec "cat $BOTTOM_FILE >> $VIDEO_FILE"
safe_exec "s3cmd put --mime-type=\"text/html\" --acl-public --continue --check-md5 '$VIDEO_FILE' 's3://${S3_BUCKET}/tools/data/video_list.html'"
# 处理MP3列表(新增)
safe_exec "echo '</ol>' >> $MP3_LINK_FILE"
safe_exec "cat $TOP_FILE > $MP3_HTML_FILE"
safe_exec "cat $MP3_LINK_FILE >> $MP3_HTML_FILE"
safe_exec "cat $BOTTOM_FILE >> $MP3_HTML_FILE"
safe_exec "s3cmd put --mime-type=\"text/html\" --acl-public --continue --check-md5 '$MP3_HTML_FILE' 's3://${S3_BUCKET}/tools/data/audio_list.html'"
echo -e "\n🎉 所有文件处理完成!"
if [ "$DRY_RUN" = true ]; then
echo "🔴 注意:DRY RUN 模式未实际执行任何修改,可核对上述步骤是否符合预期!"
else
echo "🟢 正式模式已完成"
echo "MP4链接列表:$LINK_FILE"
echo "MP3链接列表:$MP3_LINK_FILE"
fi
MP3_LOCAL_SUBDIR(本地MP3子目录名,默认mp3)MP3_S3_SUBDIR(S3上MP3子目录名,默认mp3/)a.mp4对应本地mp3/a.mp3,S3对应s3://桶名/video/wechat/mp3/a.mp3ffmpeg -i 源MP4 -vn -acodec libmp3lame 目标MP3生成(-vn确保只提取音频)mkdir -p避免目录不存在错误)audio/mpeg(MP3标准类型)s3_audio_links.txt)和HTML列表(audio_list.html)DRY_RUN)对MP3同样生效,方便验证命令正确性ffmpeg并能在环境变量中访问(可执行ffmpeg -version验证)mp3)权限正常,可读写ffmpeg命令中添加(例如-b:a 128k设置比特率):safe_exec "ffmpeg -y -i '$local_mp4' -vn -acodec libmp3lame -b:a 128k '$local_mp3'"
#!/usr/bin/env python3
import os
import subprocess
import time
from datetime import datetime
# -------------------------- 1. 配置参数(必改) --------------------------
LOCAL_DIR = "/home/nick/.wine/drive_c/users/nick/Documents/wechat" # 本地视频文件夹
S3_BUCKET = "www.staroceans.org" # S3桶名
S3_DIR = "video/wechat/" # S3目标目录(末尾加/)
MP3_LOCAL_SUBDIR = "mp3" # 本地MP3子目录名
MP3_S3_SUBDIR = "mp3/" # S3上MP3子目录名(末尾加/)
# -------------------------- 2. 测试模式开关(选填) --------------------------
DRY_RUN = True # 测试时改True(只打印不执行),正式上传改False
# -------------------------- 3. 链接文件路径配置 --------------------------
LINK_FILE = "/home/nick/diabloforum/tools/data/s3_video_links.txt"
MP3_LINK_FILE = "/home/nick/diabloforum/tools/data/s3_audio_links.txt"
TOP_FILE = "/home/nick/diabloforum/tools/data/top.txt"
BOTTOM_FILE = "/home/nick/diabloforum/tools/data/bottom.txt"
VIDEO_FILE = "/home/nick/diabloforum/tools/data/video_list.html"
MP3_VIDEO_FILE = "/home/nick/diabloforum/tools/data/audio_list.html"
def safe_exec(cmd, shell=True, check=True):
"""安全执行命令,根据DRY_RUN决定是否实际执行"""
print(f"[{'DRY RUN' if DRY_RUN else '实际执行'}] {cmd}")
if not DRY_RUN:
result = subprocess.run(
cmd,
shell=shell,
check=check,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
return result
return None
def get_file_md5(file_path):
"""获取文件MD5值"""
result = safe_exec(f"md5sum {file_path}", check=True)
if result:
return result.stdout.split()[0]
return "模拟MD5" # 测试模式返回占位符
def s3_file_exists(s3_path):
"""检查S3上文件是否存在"""
result = safe_exec(f"s3cmd info {s3_path}", check=False)
if DRY_RUN:
return False # 测试模式默认不存在,可手动修改
return result.returncode == 0
def get_s3_md5(s3_path):
"""获取S3文件的MD5值"""
result = safe_exec(f"s3cmd info {s3_path}", check=True)
if result:
for line in result.stdout.splitlines():
if "MD5 sum" in line:
return line.split(": ")[1].strip()
return "模拟S3 MD5" # 测试模式返回占位符
def get_file_timestamp(file_path):
"""获取文件最后修改时间戳"""
if not os.path.exists(file_path) and not DRY_RUN:
raise FileNotFoundError(f"文件不存在: {file_path}")
# 测试模式返回当前时间
return datetime.fromtimestamp(os.path.getmtime(file_path)).strftime("%Y-%m-%d %H:%M:%S")
def process_mp4(local_mp4):
"""处理MP4文件:检查S3存在性、上传、生成链接"""
filename = os.path.basename(local_mp4)
s3_mp4_path = f"s3://{S3_BUCKET}/{S3_DIR}{filename}"
s3_mp4_url = f"http://{S3_BUCKET}/{S3_DIR}{filename}"
print(f"\n=== 处理MP4文件:{filename} ===")
print(f"[步骤1] 检查S3是否存在: {s3_mp4_path}")
# 检查S3存在性
s3_exists = s3_file_exists(s3_mp4_path)
s3_md5_check = False # 可改为True启用MD5校验
# MD5校验逻辑
if s3_exists and s3_md5_check:
local_md5 = get_file_md5(local_mp4)
s3_md5 = get_s3_md5(s3_mp4_path)
print(f"[对比] 本地MD5: {local_md5} | S3 MD5: {s3_md5}")
if local_md5 != s3_md5:
print("[结论] MD5不一致,需要重新上传")
s3_exists = False
else:
print("[结论] MD5一致,跳过上传")
# 上传文件
if not s3_exists:
print("[步骤2] 开始上传到S3")
safe_exec(
f"s3cmd put --mime-type=\"video/mp4\" --acl-public --continue --check-md5 "
f"'{local_mp4}' '{s3_mp4_path}'"
)
# 生成链接
timestamp = get_file_timestamp(local_mp4)
link_line = f"<li><a href=\"{s3_mp4_url}\">{filename} ({timestamp})</a></li>"
safe_exec(f"echo '{link_line}' >> {LINK_FILE}")
print("[步骤3] 已添加链接到列表")
def process_mp3(local_mp4):
"""处理MP3文件:生成、检查S3存在性、上传、生成链接"""
filename = os.path.basename(local_mp4)
filename_noext = os.path.splitext(filename)[0]
mp3_filename = f"{filename_noext}.mp3"
# 路径计算
local_mp3_dir = os.path.join(LOCAL_DIR, MP3_LOCAL_SUBDIR)
local_mp3 = os.path.join(local_mp3_dir, mp3_filename)
s3_mp3_path = f"s3://{S3_BUCKET}/{S3_DIR}{MP3_S3_SUBDIR}{mp3_filename}"
s3_mp3_url = f"http://{S3_BUCKET}/{S3_DIR}{MP3_S3_SUBDIR}{mp3_filename}"
print(f"\n=== 处理MP3文件:{mp3_filename} ===")
# 确保本地MP3目录存在
safe_exec(f"mkdir -p {local_mp3_dir}")
# 检查并生成本地MP3
print(f"[步骤1] 检查本地MP3: {local_mp3}")
if not os.path.exists(local_mp3) or DRY_RUN:
if not DRY_RUN and not os.path.exists(local_mp3):
print("[生成] 本地不存在,使用ffmpeg提取音频")
safe_exec(
f"ffmpeg -y -i '{local_mp4}' -vn -acodec libmp3lame '{local_mp3}'"
)
else:
print("[DRY RUN] 模拟生成MP3文件")
# 检查S3存在性
print(f"[步骤2] 检查S3是否存在: {s3_mp3_path}")
s3_exists = s3_file_exists(s3_mp3_path)
s3_md5_check = False # 可改为True启用MD5校验
# MD5校验逻辑
if s3_exists and s3_md5_check:
local_md5 = get_file_md5(local_mp3)
s3_md5 = get_s3_md5(s3_mp3_path)
print(f"[对比] 本地MD5: {local_md5} | S3 MD5: {s3_md5}")
if local_md5 != s3_md5:
print("[结论] MD5不一致,需要重新上传")
s3_exists = False
else:
print("[结论] MD5一致,跳过上传")
# 上传文件
if not s3_exists:
print("[步骤3] 开始上传到S3")
safe_exec(
f"s3cmd put --mime-type=\"audio/mpeg\" --acl-public --continue --check-md5 "
f"'{local_mp3}' '{s3_mp3_path}'"
)
# 生成链接
timestamp = get_file_timestamp(local_mp3)
link_line = f"<li><a href=\"{s3_mp3_url}\">{mp3_filename} ({timestamp})</a></li>"
safe_exec(f"echo '{link_line}' >> {MP3_LINK_FILE}")
print("[步骤4] 已添加链接到列表")
def main():
# 初始化链接文件
print("========================================")
if DRY_RUN:
print("🔴 当前为DRY RUN测试模式,不会实际执行操作")
else:
print("🟢 当前为正式模式,将执行实际操作")
# 清空并初始化链接文件
safe_exec(f"> {LINK_FILE}")
safe_exec(f"echo '<ol>' > {LINK_FILE}")
safe_exec(f"> {MP3_LINK_FILE}")
safe_exec(f"echo '<ol>' > {MP3_LINK_FILE}")
print(f"本地目录: {LOCAL_DIR}")
print(f"本地MP3目录: {os.path.join(LOCAL_DIR, MP3_LOCAL_SUBDIR)}")
print(f"S3 MP4目标: s3://{S3_BUCKET}/{S3_DIR}")
print(f"S3 MP3目标: s3://{S3_BUCKET}/{S3_DIR}{MP3_S3_SUBDIR}")
print("========================================")
# 获取并排序本地MP4文件(按自然排序)
mp4_files = []
for file in os.listdir(LOCAL_DIR):
if file.lower().endswith(".mp4"):
mp4_files.append(os.path.join(LOCAL_DIR, file))
# 按自然排序(1→9→10→11)
mp4_files.sort(key=lambda x: int(''.join(filter(str.isdigit, os.path.basename(x))) or 0))
# 处理每个MP4文件
for mp4_file in mp4_files:
process_mp4(mp4_file)
process_mp3(mp4_file)
print("----------------------------------------")
# 生成最终HTML文件
print("\n[生成HTML列表]")
# 处理MP4 HTML
safe_exec(f"echo '</ol>' >> {LINK_FILE}")
safe_exec(f"cat {TOP_FILE} > {VIDEO_FILE}")
safe_exec(f"cat {LINK_FILE} >> {VIDEO_FILE}")
safe_exec(f"cat {BOTTOM_FILE} >> {VIDEO_FILE}")
safe_exec(
f"s3cmd put --mime-type=\"text/html\" --acl-public --continue --check-md5 "
f"'{VIDEO_FILE}' 's3://{S3_BUCKET}/tools/data/video_list.html'"
)
# 处理MP3 HTML
safe_exec(f"echo '</ol>' >> {MP3_LINK_FILE}")
safe_exec(f"cat {TOP_FILE} > {MP3_VIDEO_FILE}")
safe_exec(f"cat {MP3_LINK_FILE} >> {MP3_VIDEO_FILE}")
safe_exec(f"cat {BOTTOM_FILE} >> {MP3_VIDEO_FILE}")
safe_exec(
f"s3cmd put --mime-type=\"text/html\" --acl-public --continue --check-md5 "
f"'{MP3_VIDEO_FILE}' 's3://{S3_BUCKET}/tools/data/audio_list.html'"
)
print("\n🎉 所有文件处理完成!")
if DRY_RUN:
print("🔴 注意:测试模式未执行实际操作,请核对命令是否正确")
else:
print(f"🟢 MP4链接列表: {LINK_FILE}")
print(f"🟢 MP3链接列表: {MP3_LINK_FILE}")
if __name__ == "__main__":
main()
subprocess安全执行命令,避免注入风险python3、ffmpeg、s3cmd(已配置好S3访问权限)LOCAL_DIR、S3_BUCKET等)DRY_RUN = True,确认命令无误后改为False执行实际操作ffmpeg -version)s3cmd --configure)-b:a 128k设置比特率)get_file_timestamp时尝试读取真实文件的修改时间,导致文件不存在的错误。#!/usr/bin/env python3
import os
import subprocess
import time
from datetime import datetime
# -------------------------- 1. 配置参数(必改) --------------------------
LOCAL_DIR = "/home/nick/.wine/drive_c/users/nick/Documents/wechat" # 本地视频文件夹
S3_BUCKET = "www.staroceans.org" # S3桶名
S3_DIR = "video/wechat/" # S3目标目录(末尾加/)
MP3_LOCAL_SUBDIR = "mp3" # 本地MP3子目录名
MP3_S3_SUBDIR = "mp3/" # S3上MP3子目录名(末尾加/)
# -------------------------- 2. 测试模式开关(选填) --------------------------
DRY_RUN = True # 测试时改True(只打印不执行),正式上传改False
# -------------------------- 3. 链接文件路径配置 --------------------------
LINK_FILE = "/home/nick/diabloforum/tools/data/s3_video_links.txt"
MP3_LINK_FILE = "/home/nick/diabloforum/tools/data/s3_audio_links.txt"
TOP_FILE = "/home/nick/diabloforum/tools/data/top.txt"
BOTTOM_FILE = "/home/nick/diabloforum/tools/data/bottom.txt"
VIDEO_FILE = "/home/nick/diabloforum/tools/data/video_list.html"
MP3_VIDEO_FILE = "/home/nick/diabloforum/tools/data/audio_list.html"
def safe_exec(cmd, shell=True, check=True):
"""安全执行命令,根据DRY_RUN决定是否实际执行"""
print(f"[{'DRY RUN' if DRY_RUN else '实际执行'}] {cmd}")
if not DRY_RUN:
result = subprocess.run(
cmd,
shell=shell,
check=check,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
return result
return None
def get_file_md5(file_path):
"""获取文件MD5值"""
result = safe_exec(f"md5sum {file_path}", check=True)
if result:
return result.stdout.split()[0]
return "模拟MD5" # 测试模式返回占位符
def s3_file_exists(s3_path):
"""检查S3上文件是否存在"""
result = safe_exec(f"s3cmd info {s3_path}", check=False)
if DRY_RUN:
return False # 测试模式默认不存在
return result.returncode == 0
def get_s3_md5(s3_path):
"""获取S3文件的MD5值"""
result = safe_exec(f"s3cmd info {s3_path}", check=True)
if result:
for line in result.stdout.splitlines():
if "MD5 sum" in line:
return line.split(": ")[1].strip()
return "模拟S3 MD5" # 测试模式返回占位符
def get_file_timestamp(file_path):
"""获取文件最后修改时间戳(DRY_RUN模式返回模拟时间)"""
if DRY_RUN:
# 测试模式:返回当前时间作为模拟时间
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
if not os.path.exists(file_path):
raise FileNotFoundError(f"文件不存在: {file_path}")
return datetime.fromtimestamp(os.path.getmtime(file_path)).strftime("%Y-%m-%d %H:%M:%S")
def process_mp4(local_mp4):
"""处理MP4文件:检查S3存在性、上传、生成链接"""
filename = os.path.basename(local_mp4)
s3_mp4_path = f"s3://{S3_BUCKET}/{S3_DIR}{filename}"
s3_mp4_url = f"http://{S3_BUCKET}/{S3_DIR}{filename}"
print(f"\n=== 处理MP4文件:{filename} ===")
print(f"[步骤1] 检查S3是否存在: {s3_mp4_path}")
# 检查S3存在性
s3_exists = s3_file_exists(s3_mp4_path)
s3_md5_check = False # 可改为True启用MD5校验
# MD5校验逻辑
if s3_exists and s3_md5_check:
local_md5 = get_file_md5(local_mp4)
s3_md5 = get_s3_md5(s3_mp4_path)
print(f"[对比] 本地MD5: {local_md5} | S3 MD5: {s3_md5}")
if local_md5 != s3_md5:
print("[结论] MD5不一致,需要重新上传")
s3_exists = False
else:
print("[结论] MD5一致,跳过上传")
# 上传文件
if not s3_exists:
print("[步骤2] 开始上传到S3")
safe_exec(
f"s3cmd put --mime-type=\"video/mp4\" --acl-public --continue --check-md5 "
f"'{local_mp4}' '{s3_mp4_path}'"
)
# 生成链接
timestamp = get_file_timestamp(local_mp4)
link_line = f"<li><a href=\"{s3_mp4_url}\">{filename} ({timestamp})</a></li>"
safe_exec(f"echo '{link_line}' >> {LINK_FILE}")
print("[步骤3] 已添加链接到列表")
def process_mp3(local_mp4):
"""处理MP3文件:生成、检查S3存在性、上传、生成链接"""
filename = os.path.basename(local_mp4)
filename_noext = os.path.splitext(filename)[0]
mp3_filename = f"{filename_noext}.mp3"
# 路径计算
local_mp3_dir = os.path.join(LOCAL_DIR, MP3_LOCAL_SUBDIR)
local_mp3 = os.path.join(local_mp3_dir, mp3_filename)
s3_mp3_path = f"s3://{S3_BUCKET}/{S3_DIR}{MP3_S3_SUBDIR}{mp3_filename}"
s3_mp3_url = f"http://{S3_BUCKET}/{S3_DIR}{MP3_S3_SUBDIR}{mp3_filename}"
print(f"\n=== 处理MP3文件:{mp3_filename} ===")
# 确保本地MP3目录存在
safe_exec(f"mkdir -p {local_mp3_dir}")
# 检查并生成本地MP3
print(f"[步骤1] 检查本地MP3: {local_mp3}")
if not os.path.exists(local_mp3) or DRY_RUN:
if not DRY_RUN and not os.path.exists(local_mp3):
print("[生成] 本地不存在,使用ffmpeg提取音频")
safe_exec(
f"ffmpeg -y -i '{local_mp4}' -vn -acodec libmp3lame '{local_mp3}'"
)
else:
print("[DRY RUN] 模拟生成MP3文件")
# 检查S3存在性
print(f"[步骤2] 检查S3是否存在: {s3_mp3_path}")
s3_exists = s3_file_exists(s3_mp3_path)
s3_md5_check = False # 可改为True启用MD5校验
# MD5校验逻辑
if s3_exists and s3_md5_check:
local_md5 = get_file_md5(local_mp3)
s3_md5 = get_s3_md5(s3_mp3_path)
print(f"[对比] 本地MD5: {local_md5} | S3 MD5: {s3_md5}")
if local_md5 != s3_md5:
print("[结论] MD5不一致,需要重新上传")
s3_exists = False
else:
print("[结论] MD5一致,跳过上传")
# 上传文件
if not s3_exists:
print("[步骤3] 开始上传到S3")
safe_exec(
f"s3cmd put --mime-type=\"audio/mpeg\" --acl-public --continue --check-md5 "
f"'{local_mp3}' '{s3_mp3_path}'"
)
# 生成链接
timestamp = get_file_timestamp(local_mp3)
link_line = f"<li><a href=\"{s3_mp3_url}\">{mp3_filename} ({timestamp})</a></li>"
safe_exec(f"echo '{link_line}' >> {MP3_LINK_FILE}")
print("[步骤4] 已添加链接到列表")
def main():
# 初始化链接文件
print("========================================")
if DRY_RUN:
print("🔴 当前为DRY RUN测试模式,不会实际执行操作")
else:
print("🟢 当前为正式模式,将执行实际操作")
# 清空并初始化链接文件
safe_exec(f"> {LINK_FILE}")
safe_exec(f"echo '<ol>' > {LINK_FILE}")
safe_exec(f"> {MP3_LINK_FILE}")
safe_exec(f"echo '<ol>' > {MP3_LINK_FILE}")
print(f"本地目录: {LOCAL_DIR}")
print(f"本地MP3目录: {os.path.join(LOCAL_DIR, MP3_LOCAL_SUBDIR)}")
print(f"S3 MP4目标: s3://{S3_BUCKET}/{S3_DIR}")
print(f"S3 MP3目标: s3://{S3_BUCKET}/{S3_DIR}{MP3_S3_SUBDIR}")
print("========================================")
# 获取并排序本地MP4文件(按自然排序)
mp4_files = []
for file in os.listdir(LOCAL_DIR):
if file.lower().endswith(".mp4"):
mp4_files.append(os.path.join(LOCAL_DIR, file))
# 按自然排序(1→9→10→11)
mp4_files.sort(key=lambda x: int(''.join(filter(str.isdigit, os.path.basename(x))) or 0))
# 处理每个MP4文件
for mp4_file in mp4_files:
process_mp4(mp4_file)
process_mp3(mp4_file)
print("----------------------------------------")
# 生成最终HTML文件
print("\n[生成HTML列表]")
# 处理MP4 HTML
safe_exec(f"echo '</ol>' >> {LINK_FILE}")
safe_exec(f"cat {TOP_FILE} > {VIDEO_FILE}")
safe_exec(f"cat {LINK_FILE} >> {VIDEO_FILE}")
safe_exec(f"cat {BOTTOM_FILE} >> {VIDEO_FILE}")
safe_exec(
f"s3cmd put --mime-type=\"text/html\" --acl-public --continue --check-md5 "
f"'{VIDEO_FILE}' 's3://{S3_BUCKET}/tools/data/video_list.html'"
)
# 处理MP3 HTML
safe_exec(f"echo '</ol>' >> {MP3_LINK_FILE}")
safe_exec(f"cat {TOP_FILE} > {MP3_VIDEO_FILE}")
safe_exec(f"cat {MP3_LINK_FILE} >> {MP3_VIDEO_FILE}")
safe_exec(f"cat {BOTTOM_FILE} >> {MP3_VIDEO_FILE}")
safe_exec(
f"s3cmd put --mime-type=\"text/html\" --acl-public --continue --check-md5 "
f"'{MP3_VIDEO_FILE}' 's3://{S3_BUCKET}/tools/data/audio_list.html'"
)
print("\n🎉 所有文件处理完成!")
if DRY_RUN:
print("🔴 注意:测试模式未执行实际操作,请核对命令是否正确")
else:
print(f"🟢 MP4链接列表: {LINK_FILE}")
print(f"🟢 MP3链接列表: {MP3_LINK_FILE}")
if __name__ == "__main__":
main()
get_file_timestamp函数,增加了对DRY_RUN模式的判断:def get_file_timestamp(file_path):
"""获取文件最后修改时间戳(DRY_RUN模式返回模拟时间)"""
if DRY_RUN:
# 测试模式:返回当前时间作为模拟时间
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
if not os.path.exists(file_path):
raise FileNotFoundError(f"文件不存在: {file_path}")
return datetime.fromtimestamp(os.path.getmtime(file_path)).strftime("%Y-%m-%d %H:%M:%S")
find "$LOCAL_DIR" -maxdepth 1 -type f,然后通过管道处理,但没有过滤 MP4,可能这就是少文件的原因之一?用户可能只需要处理 MP4 文件,所以先过滤出.mp4 的。find "$LOCAL_DIR" -maxdepth 1 -type f -name "*.mp4",这样确保只处理 MP4 文件。#!/bin/bash
set -e
# -------------------------- 1. 配置参数(必改) --------------------------
LOCAL_DIR="/home/nick/.wine/drive_c/users/nick/Documents/wechat" # 本地视频文件夹
S3_BUCKET="www.staroceans.org" # S3桶名
S3_DIR="video/wechat/" # S3目标目录(末尾加/,如"videos/2024/")
# -------------------------- 2. 测试模式开关(选填) --------------------------
DRY_RUN=true # 👉 测试时改 true(只打印不执行),正式上传改 false
# --------------------------------------------------------------------------
# 链接列表文件路径(Dry Run 时也会打印路径,但不实际创建)
LINK_FILE="/home/nick/diabloforum/tools/data/s3_video_links.txt"
TOP_FILE="/home/nick/diabloforum/tools/data/top.txt"
BOTTOM_FILE="/home/nick/diabloforum/tools/data/bottom.txt"
VIDEO_FILE="/home/nick/diabloforum/tools/data/video_list.html"
# -------------------------- 核心函数:安全执行命令 --------------------------
safe_exec() {
local cmd="$*"
if [ "$DRY_RUN" = true ]; then
echo "[DRY RUN] 即将执行:$cmd"
else
echo "[实际执行] $cmd"
eval "$cmd"
fi
}
echo "========================================"
if [ "$DRY_RUN" = true ]; then
echo "🔴 当前为 DRY RUN 测试模式,不会实际上传/写文件!"
else
echo "🟢 当前为正式上传模式,会执行上传和写链接文件!"
safe_exec "> $LINK_FILE" # 正式模式:清空旧链接文件
safe_exec "echo '<ol>' > $LINK_FILE"
fi
echo "本地目录:$LOCAL_DIR"
echo "S3目标:s3://${S3_BUCKET}/${S3_DIR}"
echo "链接列表文件:$LINK_FILE"
# -------------------------- 新增:统计MP4文件总数 --------------------------
# 只统计本地目录下的.mp4文件(排除子目录)
total_mp4=$(find "$LOCAL_DIR" -maxdepth 1 -type f -name "*.mp4" | wc -l)
echo "📊 本地目录下的MP4文件总数:$total_mp4 个" # 打印总数
echo "========================================"
# 初始化已处理文件计数器
processed_mp4=0
# 1. 按人类可读顺序遍历本地MP4文件(只处理.mp4,1→9→10→11)
# 注意:新增 -name "*.mp4" 过滤,只处理MP4文件
find "$LOCAL_DIR" -maxdepth 1 -type f -name "*.mp4" | sort -V | while read -r local_file; do
# 计数器递增
processed_mp4=$((processed_mp4 + 1))
filename=$(basename "$local_file")
s3_path="s3://${S3_BUCKET}/${S3_DIR}${filename}"
s3_url="http://${S3_BUCKET}/${S3_DIR}${filename}"
echo -e "\n=== 处理文件($processed_mp4/$total_mp4):$filename ===" # 显示当前进度
# 2. 检查S3文件是否存在
echo "[步骤1] 检查S3是否存在文件:$s3_path"
if [ "$DRY_RUN" = true ]; then
s3cmd info "$s3_path"
s3_exists=false # 测试模式:模拟文件不存在
else
if s3cmd info "$s3_path" >/dev/null 2>&1; then
s3_exists=true
else
s3_exists=false
fi
fi
s3_md5_check=false
# 3. 根据文件是否存在执行逻辑
if [ "$s3_exists" = true ] && [ "$s3_md5_check" = true ]; then
echo "[结果] S3已存在该文件,准备对比MD5完整性"
local_md5=$(md5sum "$local_file" | awk '{print $1}')
if [ "$DRY_RUN" = true ]; then
s3_md5="模拟S3文件的MD5值"
else
s3_md5=$(s3cmd info "$s3_path" | grep "MD5 sum" | awk '{print $3}')
fi
echo "[对比] 本地MD5:$local_md5 | S3 MD5:$s3_md5"
if [ "$local_md5" = "$s3_md5" ] && [ "$DRY_RUN" = false ]; then
echo "[结论] MD5一致,跳过上传"
s3_exists=true
elif [ "$local_md5" != "$s3_md5" ]; then
echo "[结论] MD5不一致,需要重新上传"
s3_exists=false
else
echo "[DRY RUN] 模拟MD5一致,跳过上传并写链接"
fi
fi
if [ "$s3_exists" = false ]; then
echo "[结果] S3不存在该文件,准备上传"
safe_exec "s3cmd put --mime-type=\"video/mp4\" --acl-public --continue --check-md5 '$local_file' '$s3_path'"
fi
# 写入链接
timestamp=$(stat --format="%y" "$local_file" | cut -d. -f1)
safe_exec "echo '<li><a href=\"$s3_url\">$filename ($timestamp)</a></li>' >> $LINK_FILE"
echo "------------------------"
done
# 生成最终HTML
safe_exec "echo '</ol>' >> $LINK_FILE"
safe_exec "cat $TOP_FILE > $VIDEO_FILE"
safe_exec "cat $LINK_FILE >> $VIDEO_FILE"
safe_exec "cat $BOTTOM_FILE >> $VIDEO_FILE"
safe_exec "s3cmd put --mime-type=\"text/html\" --acl-public --continue --check-md5 '$VIDEO_FILE' 's3://${S3_BUCKET}/tools/data/video_list.html'"
# -------------------------- 新增:输出处理结果统计 --------------------------
echo -e "\n📊 处理结果统计:"
echo "本地MP4总数:$total_mp4 个"
echo "实际处理的MP4数量:$processed_mp4 个" # 对比总数和处理数,确认是否有遗漏
echo -e "\n🎉 所有文件处理完成!"
if [ "$DRY_RUN" = true ]; then
echo "🔴 注意:DRY RUN 模式未实际执行任何修改,可核对上述步骤是否符合预期!"
else
echo "🟢 正式模式已完成,链接列表保存在:$LINK_FILE"
fi
find命令中添加 -name "*.mp4" 过滤,避免处理非MP4文件导致计数混乱。find ... | wc -l计算本地目录下的MP4文件总数,赋值给total_mp4并打印。processed_mp4计数器,每处理一个文件就+1,在处理日志中显示当前进度(如处理文件(2/5):xxx.mp4)。本地MP4总数和实际处理的MP4数量,方便你直观判断是否有文件被遗漏(比如总数5个但只处理了2个,说明中间有过滤或逻辑问题)。len() 函数(不是 size(),size() 是 numpy 数组的方法)。可以在 main() 函数中添加统计代码,具体如下:len(mp4_files) 获取列表长度,添加打印语句 print(f"📊 发现的MP4文件总数: {len(mp4_files)} 个")processed_count 计数器,在循环中打印当前处理的是第几个文件(如 开始处理第 2/5 个文件)共发现 X 个MP4文件,实际处理 Y 个,方便确认是否所有文件都被处理os.listdir(LOCAL_DIR) 可能漏读了文件(比如权限问题或文件名特殊字符)continue 或异常导致文件被跳过mp4_files.sort(key=lambda x: int(''.join(filter(str.isdigit, os.path.basename(x))) or 0)),这个排序是基于文件名中的数字部分,如果文件名没有数字或数字位置不同,可能导致排序不符合预期。打印出来后用户可以明确看到问题所在。a1.mp4、a10.mp4、a2.mp4,当前算法会排成 a1.mp4、a2.mp4、a10.mp4(正确的自然排序)key函数。file_1.mp4、file_2.mp4)排序错误,可能需要调整数字提取逻辑mp4_files.sort()的key参数。key=lambda x: int(''.join(filter(str.isdigit, os.path.basename(x))) or 0)。or 0,所以它们的排序 key 都是 0。那为什么没排在一起呢?因为当 key 相同时,Python 的 sort 会按照原始字符串的字典序来排。mp4_files.sort(key=lambda x: int(''.join(filter(str.isdigit, os.path.basename(x))) or 0)
0 处理;当数字相同(均为 0)时,按文件名原始字符串的「字典序」排序。0;mp4_files.sort(...)),新增排序函数并应用:def get_file_sort_key(file_path):
"""自定义排序key:优先按系列前缀分组,再按数字/关键词排序"""
filename = os.path.basename(file_path)
# -------------------------- 关键:定义需要优先分组的系列前缀 --------------------------
# 可根据实际文件系列扩展(如新增"天棚工程"、"大模型RAG"等)
series_prefixes = [
"硅谷七姐妹分析之", # 目标系列:让该前缀的文件聚在一起
"天棚工程",
"天幕工程",
"大模型RAG"
]
# 1. 提取系列前缀(作为第一排序维度,同前缀优先)
matched_prefix = "" # 存储匹配到的系列前缀,无匹配则为空
for prefix in series_prefixes:
if filename.startswith(prefix):
matched_prefix = prefix
break
# 2. 提取系列前缀后的内容(用于同系列内排序)
if matched_prefix:
# 去掉前缀和后缀.mp4,保留核心关键词(如"英伟达"、"微软")
core_keyword = filename[len(matched_prefix):].replace(".mp4", "")
else:
# 非系列文件,核心关键词为完整文件名(去掉.mp4)
core_keyword = filename.replace(".mp4", "")
# 3. 提取文件名中的数字(原有逻辑,用于有数字的文件排序)
file_digits = ''.join(filter(str.isdigit, filename))
digit_key = int(file_digits) if file_digits else 0 # 无数字则为0
# -------------------------- 最终排序维度(优先级从高到低) --------------------------
# 1. matched_prefix:同系列前缀优先(空字符串 < 有前缀)
# 2. digit_key:同系列内按数字排序(无数字为0)
# 3. core_keyword:同系列无数字时,按关键词中文字典序排序
return (matched_prefix, digit_key, core_keyword)
# 替换原有的排序代码,使用自定义key
mp4_files.sort(key=get_file_sort_key)
...
20. 硅谷七姐妹分析之英伟达.mp4 # matched_prefix="硅谷七姐妹分析之",core_keyword="英伟达"
21. 硅谷七姐妹分析之微软.mp4 # 同前缀,core_keyword="微软"(字典序在"英伟达"后)
...(其他系列文件或非系列文件)
series_prefixes 中保留 “硅谷七姐妹分析之”,所有该前缀的文件都会自动聚在一起,并按关键词字典序排序,无需额外修改代码。natural_sort_key函数.mp4后缀,只处理核心名称;(主体部分, 数字部分),排序时先按主体字母顺序,再按数字大小。text/x-script-phyton(注意拼写也有误,应为 "python")。text/x-python(最常用,广泛被系统和工具支持)application/x-python-script(较少见,部分场景使用)text/x-script-python 并非标准定义,可能是某些特定系统或工具的非标准用法,不建议使用。在大多数场景(如服务器配置、文件类型识别)中,优先使用 text/x-python 即可。sort -V(版本排序)实现人类习惯的排序逻辑,例如:set -euo pipefail确保脚本在错误时退出,避免意外执行uploader.sh并添加执行权限:chmod +x uploader.sh./uploader.sh(默认 DRY_RUN=true)DRY_RUN=false后执行./uploader.shs3cmd(已配置 S3 访问权限)ffmpeg(用于提取 MP3 音频)<head>中添加缓存控制标签,告诉浏览器不缓存页面内容:<head>
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
</head>
Cache-Control: no-cache:强制浏览器验证资源有效性后再使用no-store:禁止浏览器存储任何版本的资源must-revalidate:资源过期后必须从服务器重新获取Pragma: no-cache:兼容 HTTP/1.0 协议Expires: 0:设置过期时间为过去(立即过期)nginx.conf或站点配置中添加:location / {
# 禁止缓存所有内容
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
}
<IfModule mod_headers.c>
Header set Cache-Control "no-cache, no-store, must-revalidate"
Header set Pragma "no-cache"
Header set Expires "0"
</IfModule>
<!-- CSS -->
<link rel="stylesheet" href="style.css?v=20231006">
<!-- JS -->
<script src="app.js?v=<?php echo time(); ?>"></script> <!-- 动态生成时间戳 -->
<!-- 图片 -->
<img src="logo.png?version=1.2.3" alt="Logo">
v=20231007),强制浏览器重新请求<meta http-equiv="expires" content="timestamp"> 是 HTML 中用于设置页面过期时间的元标签,作用是告诉浏览器:当前页面在指定的 timestamp 时间之前可以使用缓存,过期后必须重新从服务器获取最新内容。content 值(即 timestamp),并将页面缓存起来。在 timestamp 时间之前,再次访问该页面时,浏览器会直接使用本地缓存,不向服务器发送请求;超过这个时间后,浏览器会认为缓存失效,必须重新从服务器请求页面。timestamp 的格式要求content 的值必须是 GMT 时间格式(而非本地时间或时间戳),例如:<meta http-equiv="expires" content="Wed, 21 Oct 2025 07:28:00 GMT">
Expires 字段,但优先级低于服务器实际返回的 Expires 或 Cache-Control 响应头。如果服务器设置了 Cache-Control: max-age=xxx 或 Expires 头,会覆盖这个 meta 标签的设置。Cache-Control)来控制缓存。Cache-Control 响应头(如 Cache-Control: no-cache, must-revalidate),配合该标签作为补充。