music(chord): 选搜索结果里的功能谱(数字级数版本),不要字母谱
deploy music / build-and-deploy (push) Successful in 1m50s

yopu 搜索结果同一首歌通常有多个版本,区分方式:
- 字母谱:nier-snippet 里 SVG <text> 渲染 chord 字母(G/Em7/C 等)
- 功能谱:nier-snippet 里没 SVG <text>,直接 HTML/CSS 显示 1/4/5/6m

按 svgTextCount === 0 优先选第一个功能谱,没功能谱才 fallback 到字母谱。
view 页里没有「谱面样式」「和弦样式」row(要登录 APP 才有),所以这是唯一可行路径。

实测 独家记忆/倔强/Casablanca 三首都拿到正确的功能谱截图。
This commit is contained in:
Fam Zheng
2026-05-09 23:15:41 +01:00
parent 05df371435
commit ceaa2cc839
+28 -12
View File
@@ -55,10 +55,13 @@ def setup_driver(window="1920,5000"):
def find_first_chord_chart(driver, search_url):
"""在搜索页找第一个结果(yopu 现在默认全部是和弦谱),返回 view url 和 title
"""在搜索页找最佳的「功能谱」结果
旧版 yopu UI 在 .one-line-info 里有「和弦谱」字样可以过滤,
新版(svelte 重写后)已经没有,只有调号 (G调/C调/F调)。直接取第一个 a.post-main。
yopu 现在搜索结果里同一首歌有多个版本:
- 字母谱(chord chart):nier-snippet 里有 SVG <text> 渲染的 chord 字母(G/Em7/C
- 功能谱(数字 / 级数):nier-snippet 里没 SVG <text>(用 HTML/CSS 显示数字 1/4/5
我们优先取第一个**功能谱**svgTextCount === 0),fallback 到第一个字母谱。
"""
logger.info("loading search: %s", search_url)
driver.get(search_url)
@@ -72,35 +75,48 @@ def find_first_chord_chart(driver, search_url):
var titleEl = p.querySelector('.title-line .title, .title');
var subEl = p.querySelector('.title-line .subtitle, .subtitle');
var info = p.querySelector('.one-line-info');
var snippet = p.querySelector('.nier-snippet');
var svgTextCount = snippet ? snippet.querySelectorAll('svg text').length : 0;
out.push({
href: p.href,
title: titleEl ? (titleEl.textContent || '').trim() : '',
subtitle: subEl ? (subEl.textContent || '').trim() : '',
info: info ? (info.textContent || '').trim() : '',
isFunctional: svgTextCount === 0,
svgTextCount: svgTextCount,
});
}
return out;
""")
if not hits:
logger.warning("no a.post-main found in search results — yopu DOM changed?")
logger.warning("no a.post-main found — yopu DOM changed?")
return None
# MVP:直接取第一个。前 N 个一般是同一首歌的不同 key (G/C/F),第一个通常是默认 key。
first = hits[0]
href = first['href']
# 优先功能谱
functional = [h for h in hits if h['isFunctional']]
if functional:
chosen = functional[0]
kind = 'functional'
else:
chosen = hits[0]
kind = 'letter-chord (no functional version found)'
href = chosen['href']
if href.startswith('/'):
p = urlparse(search_url)
href = f"{p.scheme}://{p.netloc}{href}"
elif not href.startswith('http'):
href = urljoin(search_url, href)
logger.info("matched %d/%d hits, picking #1: %s %s [%s]",
1, len(hits), first.get('title'), first.get('subtitle'), first.get('info'))
logger.info("[%s] %s %s [%s] (%d total: %d functional, %d letter)",
kind, chosen['title'], chosen['subtitle'], chosen['info'],
len(hits), len(functional), len(hits) - len(functional))
return {
'url': href,
'title': first.get('title') or '',
'subtitle': first.get('subtitle') or '',
'text': first.get('info') or '',
'title': chosen.get('title') or '',
'subtitle': chosen.get('subtitle') or '',
'text': chosen.get('info') or '',
'kind': kind,
}