# shellcheck shell=bash # shellcheck disable=SC2034 # 跨文件共享全局 map(MEDIA_DESTINATION_MAP 等,声明于 main.sh) # 结构上不可能出现只写一半的不一致。识别池并发运行时,子进程通过 emit 输出 # 协议行(key\tTYPE\tpayload)到 POOL_EMIT_FILE,父进程合并时调用 register_*。 # 协议行类型: # DEST → register_identified # FALLBACK → register_fallback # PENDING → register_pending(待 AI 搜索纠正) # ARTIST_PENDING → register_pending_artist(音频多艺术家待 AI 判定) # SPECIAL_PENDING → 特典映射待学习 # SKIP → 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" }