发布前用本地门禁核对 license 快照:状态 active、功能开启、时长上限、商用与署名意愿。不能替代正式合同。
先看清成品
- 合规样例 ok;streaming/超长/未商用/未署名 → 多条 reasons。
- 2 项 pytest 通过。
合规提醒:Mubert 曲目授权取决于套餐与合同;免费/低档常需署名且用途受限。发布前核对 license 与是否允许商用/变现平台。曲目下载 URL 会过期,及时落盘。本文不编造套餐价格。
运行说明:无正式 MUBERT_CUSTOMER_ID / ACCESS_TOKEN。A/B 用 mock;对 music-api.mubert.com 无效凭证实测 HTTP 401。C 为本地 license 门禁。2026-10-23(Asia/Shanghai)pytest 通过。
做完之后你会得到什么?
| 成果 | 说明 |
|---|---|
license_guard.py |
门禁 |
| 样例 JSON | license/use |
test_111c.py |
测试 |
需要准备哪些环境?
| 项目 | 说明 |
|---|---|
| Python | 3.10+ |
关键事实
| 事实 | 来源 |
|---|---|
| GET /api/v3/service/licenses 返回功能与限额 | 官方 License |
| -1 表示无限 | 官方 Docs |
| 免费档署名与用途限制以官网为准 | 产品说明 |
文件结构
111-mubert-C-src/
├── license_guard.py
├── sample_license.json
├── sample_use_ok.json
├── sample_use_bad.json
└── test_111c.py
第 1 步:门禁
#!/usr/bin/env python3
"""Local license gate before publishing Mubert tracks.
Checks a license snapshot JSON (from GET /api/v3/service/licenses) plus your use case:
commercial flag, attribution, duration vs max_track_duration, feature enabled.
Does NOT replace the official contract — only a pre-flight checklist.
"""
from __future__ import annotations
import argparse, json, sys
from pathlib import Path
def check(license_obj: dict, use: dict) -> dict:
reasons = []
data = license_obj.get("data") or license_obj
status = data.get("status")
if status != "active":
reasons.append(f"license status={status}")
features = set(data.get("features") or [])
if data.get("allow_all_features"):
features |= {"track", "ttm", "streaming"}
need = use.get("feature", "track")
if need not in features and not data.get("allow_all_features"):
reasons.append(f"feature {need} not in license")
limits = data.get("license_limits") or {}
max_dur = limits.get("max_track_duration")
dur = int(use.get("duration", 0))
if max_dur not in (None, -1) and dur > int(max_dur):
reasons.append(f"duration {dur} > max_track_duration {max_dur}")
if use.get("commercial") and not use.get("plan_allows_commercial", False):
reasons.append("commercial use not confirmed for this plan")
if use.get("needs_attribution") is True and not use.get("will_attribute", False):
reasons.append("attribution required but will_attribute=false")
return {"ok": not reasons, "reasons": reasons, "status": data.get("status"), "feature": need}
def main(argv=None) -> int:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("license_json")
ap.add_argument("use_json")
args = ap.parse_args(argv)
lic = json.loads(Path(args.license_json).read_text())
use = json.loads(Path(args.use_json).read_text())
r = check(lic, use)
print(json.dumps(r, ensure_ascii=False, indent=2))
return 0 if r["ok"] else 1
if __name__ == "__main__":
raise SystemExit(main())
第 2 步:测试
import json, subprocess, sys
from pathlib import Path
HERE = Path(__file__).resolve().parent
def run(*extra):
return subprocess.run([sys.executable, str(HERE/"license_guard.py"), *extra], capture_output=True, text=True)
def test_ok():
r = run(str(HERE/"sample_license.json"), str(HERE/"sample_use_ok.json"))
assert r.returncode == 0
assert json.loads(r.stdout)["ok"] is True
def test_bad():
r = run(str(HERE/"sample_license.json"), str(HERE/"sample_use_bad.json"))
assert r.returncode == 1
body = json.loads(r.stdout)
assert body["ok"] is False
assert len(body["reasons"]) >= 2
第 3 步:运行
$ pytest 111c
.. [100%]
2 passed in 0.06s
怎么检查结果?
- ok 样例 exit 0。
- bad 样例 reasons ≥ 2。
调试示例
「同事说可以商用」。 不够。以当前 license_limits 与合同邮件为准,脚本要求显式布尔。
常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
| 多 license | 每个产品线单独快照 | 分凭证 |
| 月配额 | 看 monthly_*_limit | 用尽就停 |
| 署名文案 | 视频简介写 Mubert | 按套餐要求 |
模板规则
- 无合同不商用。
- 超时长先改参数。
- 门禁进发布流水线。
项目资源
| 用途 | 地址 |
|---|---|
| API Docs(官方) | https://mubert.com/api/docs |
| API 产品页 | https://mubert.com/api |
| Mubert | https://mubert.com |
授权清楚,才敢把配乐推进成片。