#!/bin/bash # refresh-urls.sh — 队列地址保活 # # 网易云的播放直链是限时签名 URL:实测取出来约 5 分钟就 403 # (路径里的时间戳 + vuutv/authSecret 签名,服务端一核,过期就拒绝)。 # 长队列预取一批地址,放到后半段会集体失效,听感是「歌还在放、就是没声音」。 # # 本脚本由入口的 keepalive_start 拉起(一次只跑一个),每 90 秒看一次: # 这批地址取出来多久了(urls_at)× mpv 播到第几首(playlist-pos) # 一到 4 分钟,就把「当前这首之后还没播的」批量重取,原地换掉 m3u 里对应的行。 # mpv 是按需读取下一首的,所以换 URL 不影响正在放的那首。 # 队列被换掉 / 停止 / mpv 退出,脚本自己收工。 set -u SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" LOCAL_DIR="${NETEASE_MUSIC_STATE_DIR:-${PI_AGENT_DIR:-$HOME/.pi/agent}/local/skills/netease-music}" QUEUE="$LOCAL_DIR/queue.json" M3U="$LOCAL_DIR/queue.m3u8" URLS_AT="$LOCAL_DIR/urls_at" SOCK="$LOCAL_DIR/mpv.sock" API="$SKILL_DIR/scripts/netease-api.mjs" QUALITY="$(python3 -c 'import json,sys try: print(json.load(open(sys.argv[1])).get("quality") or "lossless") except Exception: print("lossless")' "$LOCAL_DIR/config.json" 2>/dev/null)" QUALITY="${NETEASE_MUSIC_QUALITY:-${PI_SKILL_NETEASE_MUSIC_QUALITY:-$QUALITY}}" COOKIE="${NETEASE_MUSIC_COOKIE:-${PI_SKILL_NETEASE_MUSIC_COOKIE:-$(python3 -c 'import json,sys try: print(json.load(open(sys.argv[1])).get("cookie") or "") except Exception: print("")' "$LOCAL_DIR/config.json" 2>/dev/null)}}" log() { printf '%s %s\n' "$(date '+%H:%M:%S')" "$*"; } mpv_query() { # mpv_query <属性>:拿不到就打印空 python3 - "$SOCK" "$1" <<'PY' 2>/dev/null import json, socket, sys try: s = socket.socket(socket.AF_UNIX); s.settimeout(3); s.connect(sys.argv[1]) s.sendall(json.dumps({"command": ["get_property", sys.argv[2]], "request_id": 1}).encode() + b"\n") r = json.loads(s.recv(65536).decode()) print(r.get("data") if r.get("error") == "success" else "") except Exception: print("") PY } refetch_from() { # refetch_from <起始序号 0-based>:重取这一首之后的地址,原地换 m3u local from="$1" tmp_ids tmp_urls n tmp_ids="$(mktemp)"; tmp_urls="$(mktemp)" python3 - "$QUEUE" "$from" > "$tmp_ids" <<'PY' import json, sys q = json.load(open(sys.argv[1])) for i, s in enumerate(q["songs"]): if i >= int(sys.argv[2]): print(s["id"]) PY [ -s "$tmp_ids" ] || { rm -f "$tmp_ids" "$tmp_urls"; return 1; } n="$(wc -l < "$tmp_ids" | tr -d ' ')" log "重取地址:从第 $((from + 1)) 首起共 $n 首" while read -r chunk; do NETEASE_COOKIE="$COOKIE" node "$API" url $chunk --level "$QUALITY" 2>/dev/null \ | python3 -c 'import json,sys for it in (json.load(sys.stdin).get("items") or []): print("%s\t%s" % (it.get("id"), it.get("url") or ""))' >> "$tmp_urls" done < <(python3 -c 'import sys ids = [l.strip() for l in sys.stdin if l.strip()] for i in range(0, len(ids), 50): print(" ".join(ids[i:i+50]))' < "$tmp_ids") python3 - "$M3U" "$QUEUE" "$tmp_urls" "$from" <<'PY' import json, sys mpath, qpath, upath, start = sys.argv[1], sys.argv[2], sys.argv[3], int(sys.argv[4]) urls = {} for line in open(upath): sid, _, url = line.rstrip("\n").partition("\t") if url: urls[sid] = url order = [str(s["id"]) for s in json.load(open(qpath))["songs"]] lines = open(mpath).read().split("\n") head = [i for i, l in enumerate(lines) if l.startswith("#EXTINF")] changed = 0 for pos, i in enumerate(head): if pos < start or pos >= len(order) or i + 1 >= len(lines): continue u = urls.get(order[pos]) if u: lines[i + 1] = u changed += 1 open(mpath, "w").write("\n".join(lines)) print("换成新地址的条数:%d/%d" % (changed, len(head) - start)) PY log "重取完成(从第 $((from + 1)) 首起)" rm -f "$tmp_ids" "$tmp_urls" } log "地址保活启动" while :; do sleep 90 [ -n "$(mpv_query idle-active)" ] || { log "mpv 没了,收工"; exit 0; } [ -s "$QUEUE" ] || { log "队列没了,收工"; exit 0; } age=$(( $(date +%s) - $(cat "$URLS_AT" 2>/dev/null || echo 0) )) [ "$age" -lt 240 ] && continue # 「当前放到第几首」按歌认:mpv 开了 shuffle 时 playlist-pos 不是 queue.json 的序号 title="$(mpv_query media-title)" curl="$(mpv_query path)" pos="$(python3 "$SKILL_DIR/scripts/lib/mpv-map.py" index "$QUEUE" "$LOCAL_DIR/urls.txt" "$title" "$curl" 2>/dev/null)" case "$pos" in ''|*[!0-9]*) pos=0 ;; esac if refetch_from "$pos"; then date +%s > "$URLS_AT"; else log "重取失败,90 秒后再试"; fi done