refactor: 迁移至 bashly CLI 与功能域模块结构
- CLI 定义迁移至 src/bashly.yml(bashly 1.4 生成参数解析),入口收敛为 src/root_command.sh - 源码按功能域重组:config/storage/integrate/media/pipeline,消除扁平散落 - 新增零依赖测试框架 tests/(123 用例)与 CI 流水线(lint + test + build + 产物一致性) - build.sh 支持版本单一来源注入与 --check 产物校验;新增 lint.sh(bash -n + shellcheck 零容忍) - 删除旧单文件 media_organizer.sh 与旧扁平模块
This commit is contained in:
1 parent
d978a13a83
commit
0df07ad0ae
48 files changed
+5813
-6812
No files matched your search
@@ -0,0 +1,207 @@
|
||||
# shellcheck shell=bash
|
||||
# shellcheck disable=SC2034 # 跨文件共享全局 map(MEDIA_DESTINATION_MAP 等,声明于 main.sh)
|
||||
|
||||
# 结构上不可能出现只写一半的不一致。识别池并发运行时,子进程通过 emit 输出
|
||||
# 协议行(key\tTYPE\tpayload)到 POOL_EMIT_FILE,父进程合并时调用 register_*。
|
||||
# 协议行类型:
|
||||
# DEST <subdir|filename> → register_identified
|
||||
# FALLBACK <subdir|filename> → register_fallback
|
||||
# PENDING <ai_data> → register_pending(待 AI 搜索纠正)
|
||||
# ARTIST_PENDING <artists|album> → register_pending_artist(音频多艺术家待 AI 判定)
|
||||
# SPECIAL_PENDING <show_id|frag|show|names> → 特典映射待学习
|
||||
# SKIP <reason> → register_skip(skip_type/skip_unidentified)
|
||||
# REQ_FAILED → register_request_failed
|
||||
################################################################################
|
||||
|
||||
# 登记识别成功条目(可链接)。
|
||||
register_identified() {
|
||||
local key="$1" subdir="$2" filename="$3"
|
||||
MEDIA_DESTINATION_MAP["$key"]="${subdir}|${filename}"
|
||||
MEDIA_OUTCOME_MAP["$key"]="identified"
|
||||
}
|
||||
|
||||
# 登记回退命名条目(AI 尽力后仍失败;降级成功,可链接)。
|
||||
register_fallback() {
|
||||
local key="$1" subdir="$2" filename="$3"
|
||||
MEDIA_DESTINATION_MAP["$key"]="${subdir}|${filename}"
|
||||
MEDIA_OUTCOME_MAP["$key"]="fallback"
|
||||
}
|
||||
|
||||
# 登记待 AI 搜索纠正条目(不进入目的映射)。
|
||||
register_pending() {
|
||||
local key="$1" ai_data="$2"
|
||||
PENDING_AI_SEARCH["$key"]="$ai_data"
|
||||
PENDING_SEARCH_COUNT=$((PENDING_SEARCH_COUNT + 1))
|
||||
MEDIA_OUTCOME_MAP["$key"]="pending_ai"
|
||||
}
|
||||
|
||||
# 登记待 AI 判定艺术家的音频条目(不进入目的映射)。
|
||||
register_pending_artist() {
|
||||
local key="$1" artists_album="$2"
|
||||
PENDING_AI_ARTIST["$key"]="$artists_album"
|
||||
PENDING_ARTIST_COUNT=$((PENDING_ARTIST_COUNT + 1))
|
||||
MEDIA_OUTCOME_MAP["$key"]="pending_ai"
|
||||
}
|
||||
|
||||
# 登记跳过条目(type=skip_type/skip_unidentified;不进入目的映射)。
|
||||
# skip_unidentified(AI 尽力后仍失败 / 无 AI 密钥)→ 登记失败冷却(下次运行冷却期内跳过,
|
||||
# 避免 cron 每轮全量重试;冷却过期后自动重试,语义仍可逆)。skip_type(电影特典)无成本,不冷却。
|
||||
register_skip() {
|
||||
local key="$1" reason="$2"
|
||||
MEDIA_OUTCOME_MAP["$key"]="$reason"
|
||||
if [[ "$reason" == "skip_unidentified" ]]; then
|
||||
cooldown_set "$key" "$reason"
|
||||
fi
|
||||
}
|
||||
|
||||
# 登记请求失败条目(重试耗尽仍失败;不进入目的映射)。
|
||||
# 网络/密钥类失败同样登记冷却:冷却期内跳过,过期后自动重试。
|
||||
register_request_failed() {
|
||||
local key="$1"
|
||||
MEDIA_OUTCOME_MAP["$key"]="request_failed"
|
||||
cooldown_set "$key" "request_failed"
|
||||
}
|
||||
|
||||
# 登记待 AI 匹配甄别条目(搜索有结果但需甄别;不进入目的映射)。
|
||||
# 同 key 重复登记(如纠正词重搜再次需甄别)不重复计数。
|
||||
register_pending_match() {
|
||||
local key="$1" payload="$2"
|
||||
if [[ -z "${PENDING_AI_MATCH[$key]:-}" ]]; then
|
||||
PENDING_MATCH_COUNT=$((PENDING_MATCH_COUNT + 1))
|
||||
fi
|
||||
PENDING_AI_MATCH["$key"]="$payload"
|
||||
MEDIA_OUTCOME_MAP["$key"]="pending_ai"
|
||||
}
|
||||
|
||||
# 目录链:文件父目录相对源根的路径,最多保留靠近文件的 3 层(剧名通常靠近文件)。
|
||||
# 用于 AI 输入(directory 字段)与 PENDING_AI_SEARCH 存储。
|
||||
dir_chain() {
|
||||
local file="$1" rel
|
||||
rel=$(dirname "$file")
|
||||
rel="${rel#"$SOURCE_DIR"/}"
|
||||
# 不在源目录下(或就是源根)→ 用父目录名
|
||||
if [[ "$rel" == "$(dirname "$file")" ]]; then
|
||||
rel=$(basename "$rel")
|
||||
fi
|
||||
local parts=() out="" p
|
||||
IFS='/' read -ra parts <<<"$rel"
|
||||
if ((${#parts[@]} > 3)); then
|
||||
parts=("${parts[@]:${#parts[@]}-3}")
|
||||
fi
|
||||
for p in "${parts[@]}"; do
|
||||
out="${out:+$out/}$p"
|
||||
done
|
||||
echo "$out"
|
||||
}
|
||||
|
||||
# 合并协议行文件(register_* 登记)。返回前记录 MERGE_PROCESSED/FAILED/STREAK 供阈值检查。
|
||||
# 用于:识别池合并(pool_run)与父进程 identify 调用(命令替换会丢 emit 副作用,需经临时文件回传)。
|
||||
merge_emit_lines() {
|
||||
local emit_file="$1"
|
||||
MERGE_PROCESSED=0
|
||||
MERGE_FAILED=0
|
||||
MERGE_STREAK=0
|
||||
local key type payload
|
||||
while IFS=$'\t' read -r key type payload; do
|
||||
[[ -z "$key" ]] && continue
|
||||
MERGE_PROCESSED=$((MERGE_PROCESSED + 1))
|
||||
case "$type" in
|
||||
DEST)
|
||||
local subdir filename
|
||||
IFS='|' read -r subdir filename <<<"$payload"
|
||||
register_identified "$key" "$subdir" "$filename"
|
||||
;;
|
||||
FALLBACK)
|
||||
local subdir filename
|
||||
IFS='|' read -r subdir filename <<<"$payload"
|
||||
register_fallback "$key" "$subdir" "$filename"
|
||||
;;
|
||||
PENDING)
|
||||
register_pending "$key" "$payload"
|
||||
;;
|
||||
ARTIST_PENDING)
|
||||
register_pending_artist "$key" "$payload"
|
||||
;;
|
||||
SPECIAL_PENDING)
|
||||
local show_id fragment show_title season0_names
|
||||
IFS='|' read -r show_id fragment show_title season0_names <<<"$payload"
|
||||
PENDING_AI_SPECIAL["${show_id}|${fragment}"]="${show_title}|${season0_names}"
|
||||
PENDING_SPECIAL_COUNT=$((PENDING_SPECIAL_COUNT + 1))
|
||||
;;
|
||||
SKIP)
|
||||
register_skip "$key" "$payload"
|
||||
;;
|
||||
REQ_FAILED)
|
||||
register_request_failed "$key"
|
||||
MERGE_FAILED=$((MERGE_FAILED + 1))
|
||||
MERGE_STREAK=$((MERGE_STREAK + 1))
|
||||
;;
|
||||
MATCH)
|
||||
register_pending_match "$key" "$payload"
|
||||
;;
|
||||
*)
|
||||
MERGE_STREAK=0
|
||||
;;
|
||||
esac
|
||||
done < <(cat "$emit_file")
|
||||
}
|
||||
|
||||
# 协议输出:worker 模式写入 POOL_EMIT_FILE;父进程模式(无池)直接登记。
|
||||
emit() {
|
||||
local key="${1:-}" type="${2:-}" payload="${3:-}"
|
||||
if [[ -n "${POOL_EMIT_FILE:-}" ]]; then
|
||||
printf '%s\t%s\t%s\n' "$key" "$type" "$payload" >>"$POOL_EMIT_FILE"
|
||||
return 0
|
||||
fi
|
||||
case "$type" in
|
||||
SPECIAL_PENDING)
|
||||
local show_id fragment show_title season0_names
|
||||
IFS='|' read -r show_id fragment show_title season0_names <<<"$payload"
|
||||
PENDING_AI_SPECIAL["${show_id}|${fragment}"]="${show_title}|${season0_names}"
|
||||
PENDING_SPECIAL_COUNT=$((PENDING_SPECIAL_COUNT + 1))
|
||||
;;
|
||||
MATCH)
|
||||
register_pending_match "$key" "$payload"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# 识别池:对文件列表并发执行 process_one_file,结果行经 POOL_EMIT_FILE 收集后由父进程合并。
|
||||
# 并发控制:满池时等待最老的 job(bash 4.0 兼容,不用 wait -n)。
|
||||
pool_run() {
|
||||
local pool_dir
|
||||
pool_dir=$(mktemp -d)
|
||||
export POOL_EMIT_FILE="$pool_dir/emit"
|
||||
: >"$POOL_EMIT_FILE"
|
||||
|
||||
local -a pids=()
|
||||
local f workers="${MEDIA_WORKERS:-4}"
|
||||
for f in "$@"; do
|
||||
# worker 协议只走 POOL_EMIT_FILE,stdout 丢弃(防止识别函数 echo 泄漏到终端)
|
||||
(process_one_file "$f" >/dev/null) &
|
||||
pids+=("$!")
|
||||
if ((${#pids[@]} >= workers)); then
|
||||
wait "${pids[0]}" || true
|
||||
pids=("${pids[@]:1}")
|
||||
fi
|
||||
done
|
||||
for p in "${pids[@]}"; do
|
||||
wait "$p" || true
|
||||
done
|
||||
|
||||
# 合并协议行(register_* 登记 + 请求失败统计)
|
||||
merge_emit_lines "$POOL_EMIT_FILE"
|
||||
|
||||
# 请求失败阈值(仅初始识别池启用):连续 ≥5 次或失败率 ≥50% → 终止,防止整库跑空
|
||||
if [[ "${POOL_THRESHOLD_CHECK:-false}" == "true" ]]; then
|
||||
local rate=0
|
||||
((MERGE_PROCESSED > 0)) && rate=$((MERGE_FAILED * 100 / MERGE_PROCESSED))
|
||||
if ((MERGE_STREAK >= 5 || (MERGE_PROCESSED >= 10 && rate >= 50))); then
|
||||
_log 错误 "TMDB 请求失败过多(连续 ${MERGE_STREAK} 次 / 失败率 ${rate}%),终止运行——请检查网络或 TMDB 密钥"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
unset POOL_EMIT_FILE
|
||||
rm -rf "$pool_dir"
|
||||
}
|
||||
Reference in new issue
Block a user