脚本下载在文末
很早之前就尝试搞过Navidrome,后续觉得UI太丑用着不舒服遂放弃.
上周看[初之音博客](),里面有推荐一个「音流」app,试了下感觉挺舒服的,特别戳我的是一个我愿称之为"杀手级"的功能.
接着,你应该注意到了首页「主线路」这三个字。这是音流中最让我感到惊喜的功能,它支持主备线路切换 。也就是说,你可以主线路填内网地址,备用线路填外网地址。这样当你出门的时候,音流可以自动切换到外网地址上,而当你回到家中,又自动切换回内网。由于流媒体有缓存,切换网络的过程播放音乐不会中断。
这对我这种旁路由挂ddns并还没有搞 NAT Loopback 的人(懒狗)来说简直是福音
但很快我遇到了一个问题,RT,Navidrome会把多艺人专辑识别成多专辑的问题,对于乐队番的ACG歌曲来说就很炸裂
看到评论区有老哥推music tag web, 说是可以解决多艺人专辑显示为多专辑
但其实不用这么麻烦, 根据Navidrome官方FaQ
For a “Various Artists” compilation, the Part Of Compilation tag (TCMP=1 for id3, COMPILATION=1 for FLAC) must be set, for all tracks.
然后我用chatgpt写了一个小脚本, 可以检测所在目录下及其子文件夹的FLAC是否需要添加compilation, 以及一键添加好Compilation=1.
"""
va_compilation_check.py
递归扫描指定根目录(默认:脚本所在目录)下的所有 .flac 文件:
1) 发现“由不同艺人组成的同一张专辑”(同 ALBUM,ARTIST 多于一个)。
2) 检查/修复 FLAC 的 COMPILATION=1(Part Of Compilation)。
3) 检测到需要添加时,--fix 会对“整张专辑的全部曲目”写入 COMPILATION=1。
4) 可选统一 ALBUMARTIST=Various Artists。
用法:
python va_compilation_check.py
python va_compilation_check.py --root /path/to/music
python va_compilation_check.py --fix
python va_compilation_check.py --fix --no-set-albumartist
python va_compilation_check.py --follow-symlinks
依赖:
pip install mutagen
"""
import argparse
import sys
import os
from pathlib import Path
from collections import defaultdict
from typing import Dict, List, Set, Tuple
from mutagen.flac import FLAC, FLACNoHeaderError
def get_tag_first(tags: Dict[str, List[str]], key: str, default: str = "") -> str:
for k in (key, key.upper(), key.lower(), key.title()):
if k in tags and tags[k]:
v = tags[k]
return v[0] if isinstance(v, list) else str(v)
return default
def get_tag_all(tags: Dict[str, List[str]], key: str) -> List[str]:
for k in (key, key.upper(), key.lower(), key.title()):
if k in tags:
v = tags[k]
return list(v) if isinstance(v, list) else [str(v)]
return []
def set_tag_value(audio: FLAC, key: str, value: str):
audio[key] = [value]
def is_compilation_value(val: str) -> bool:
return val.strip().lower() in {"1", "true", "yes"}
def collect_flac_files(root: Path, follow_symlinks: bool) -> List[Path]:
flacs: List[Path] = []
for dirpath, dirnames, filenames in os.walk(root, followlinks=follow_symlinks):
for fn in filenames:
if fn.lower().endswith(".flac"):
flacs.append(Path(dirpath) / fn)
return sorted(flacs)
def main():
ap = argparse.ArgumentParser(description="递归检测/修复 FLAC Various Artists 合辑的 COMPILATION=1")
default_root = Path(__file__).resolve().parent
ap.add_argument("--root", default=str(default_root),
help=f"扫描根目录(默认:脚本所在目录 {default_root})")
ap.add_argument("--fix", action="store_true", help="为识别出的合辑写入(或统一) COMPILATION=1")
ap.add_argument("--set-albumartist", dest="set_albumartist", action="store_true",
help="修复时设置 ALBUMARTIST=Various Artists(默认开启)")
ap.add_argument("--no-set-albumartist", dest="set_albumartist", action="store_false",
help="修复时不要改 ALBUMARTIST")
ap.add_argument("--follow-symlinks", action="store_true",
help="遍历时跟随符号链接目录(可能导致循环,谨慎开启)")
ap.set_defaults(set_albumartist=True)
args = ap.parse_args()
root = Path(args.root).resolve()
if not root.exists():
print(f"[错误] 路径不存在: {root}", file=sys.stderr)
sys.exit(2)
flac_files = collect_flac_files(root, args.follow_symlinks)
if not flac_files:
print("[信息] 未在该目录及其子目录发现 .flac 文件。")
return
albums: Dict[str, List[Tuple[Path, FLAC]]] = defaultdict(list)
invalid_files: List[Tuple[Path, str]] = []
for p in flac_files:
try:
audio = FLAC(p.as_posix())
except FLACNoHeaderError:
invalid_files.append((p, "不是有效的 FLAC 文件头"))
continue
except Exception as e:
invalid_files.append((p, f"读取失败: {e}"))
continue
album = get_tag_first(audio.tags or {}, "ALBUM", "").strip()
albums[album].append((p, audio))
exit_code = 0
print(f"共发现 FLAC 文件: {len(flac_files)},分属专辑(按 ALBUM):{len(albums)}\n")
if invalid_files:
print("[警告] 以下文件无效或读取失败:")
for p, reason in invalid_files:
print(f" - {p}: {reason}")
print()
for album, items in albums.items():
artists: Set[str] = set()
comp_values: List[Tuple[Path, str]] = []
albumartists: Set[str] = set()
for p, audio in items:
tags = audio.tags or {}
for a in get_tag_all(tags, "ARTIST") or [""]:
artists.add(a.strip())
comp_values.append((p, get_tag_first(tags, "COMPILATION", "").strip()))
aa = get_tag_first(tags, "ALBUMARTIST", "").strip()
if aa:
albumartists.add(aa)
album_display = album if album else "(未设置 ALBUM)"
print(f"=== 专辑:{album_display} — 曲目数 {len(items)} ===")
non_empty_artists = {a for a in artists if a}
is_va_candidate = len(non_empty_artists) > 1
print(f" 参与艺人数量:{len(non_empty_artists)}"
f"{'(含有缺失 ARTIST 的曲目)' if '' in artists else ''}")
print(" => 判定:{}"
.format("疑似 Various Artists 合辑(多艺人)" if is_va_candidate else "非合辑/信息不足"))
wrong_comp = [(p, comp) for p, comp in comp_values if not is_compilation_value(comp)]
if is_va_candidate:
if not wrong_comp:
print(" COMPILATION:全部正确(=1)。")
# 即便都为1,也可在 --fix 下统一 ALBUMARTIST(不必改 COMPILATION)
if args.fix and args.set_albumartist:
for _, audio in items:
set_tag_value(audio, "ALBUMARTIST", "Various Artists")
audio.save()
print(" [修复] 已统一 ALBUMARTIST=Various Artists(COMPILATION 原本已正确)。")
else:
exit_code = max(exit_code, 1)
print(f" COMPILATION:有 {len(wrong_comp)}/{len(items)} 首不为 1 或缺失:")
for p, comp in wrong_comp:
print(f" - {p.name}: {comp if comp else '(缺失)'}")
if args.fix:
# ⭐ 核心改动:一旦该专辑被判定需要添加,就为“整张专辑全部曲目”写入 COMPILATION=1
for _, audio in items:
set_tag_value(audio, "COMPILATION", "1")
if args.set_albumartist:
set_tag_value(audio, "ALBUMARTIST", "Various Artists")
audio.save()
print(" [修复] 已为整张专辑的所有曲目统一写入 COMPILATION=1;"
f"{'并统一 ALBUMARTIST=Various Artists。' if args.set_albumartist else '未改 ALBUMARTIST。'}")
else:
wrongly_set = [p for p, comp in comp_values if is_compilation_value(comp)]
if wrongly_set:
print(f" 注意:这似乎不是合辑,但有 {len(wrongly_set)} 首设置了 COMPILATION=1。")
print()
if exit_code == 0:
print("检查完成:未发现需要修复的合辑。")
else:
print("检查完成:发现需要修复的合辑(见上方详情)。可使用 --fix 进行整专辑统一修复。")
sys.exit(exit_code)
if __name__ == "__main__":
main()
Views: 4










