Skill: 52yuanchangxing/desktop-music-launcher (version 1.0.0)

--- name: desktop-music-launcher description: 检索本机已安装音乐软件，启动它，并根据用户需求推荐、搜索或播放歌曲；在 macOS 上可用 AppleScript 控制 Spotify 和 Apple Music，并为 Spotify 增加可选的精确点播链路。 version: 2.1.0 allowed-tools: Bash Python Read metadata: category: music tags: - music - desktop - spotify - apple-music - player - launcher - control - applescript provider: local-desktop openclaw: emoji: "🎵" os: - windows - macos - linux requires: anyBins: - python3 - python homepage: https://github.com/openclaw/clawhub/blob/main/docs/skill-format.md --- # desktop-music-launcher ## 何时使用 当用户想要： - 检查电脑里装了哪些音乐软件或播放器 - 打开 Spotify / Apple Music / QQ 音乐 / 网易云音乐 / VLC / mpv 等 - 按歌名、歌手、专辑、情绪、场景搜索音乐 - 根据“适合学习 / 跑步 / 睡前 / 聚会 / 失恋”等需求推荐可立即搜索的音乐方向 - 在 macOS 上直接控制 Spotify / Apple Music 的播放、暂停、下一首、上一首、状态查询 - 在 macOS 上对 Spotify / Apple Music 执行“按需求尽力开始播放”的 UI 自动化 ## 不做什么 - 不伪造“已经开始播放”的结果；必须以脚本返回结果为准 - 不调用未声明的私有 API、逆向接口或账号 Cookie - 不越权扫描无关目录；只检查常见安装路径和 PATH 中的可执行文件 - 不自动登录任何音乐服务，也不读取用户账号密码 - 不下载盗版资源，不绕过版权或付费限制 - 不承诺所有客户端、所有版本、所有语言 UI 下都能 100% 精确点开第一首歌 ## 运行方式 统一使用： ```bash python3 scripts/music_skill.py <command> [args] ``` 若系统只有 `python`，则改用： ```bash python scripts/music_skill.py <command> [args] ``` ## 命令总览 ### 1) 扫描本机音乐软件 ```bash python3 scripts/music_skill.py scan ``` 返回 JSON，包含当前系统、识别到的应用、是否安装、命中的路径/命令、支持的能力；在 macOS 上还会返回是否存在 `osascript`。 ### 2) 打开软件 ```bash python3 scripts/music_skill.py open --app spotify python3 scripts/music_skill.py open --app vlc ``` ### 3) 搜索歌曲 / 歌手 / 专辑 ```bash python3 scripts/music_skill.py search "周杰伦 稻香" python3 scripts/music_skill.py search "lofi beats" --app spotify --open ``` ### 4) 播放 ```bash python3 scripts/music_skill.py play "Eason Chan 富士山下" --app spotify --control-mode macos-ui python3 scripts/music_skill.py play "Taylor Swift Love Story" --app apple-music --control-mode macos-ui python3 scripts/music_skill.py play --file "~/Music/demo.mp3" --app vlc --open python3 scripts/music_skill.py play --url "https://example.com/demo.mp3" --app mpv --open ``` 行为说明： - `play <query>`： - macOS + Spotify / Apple Music 时，默认优先尝试控制版的 UI 自动化 - 其他系统或不支持控制的客户端，则退化为 URI / 网页搜索 - `play --file`：把本地音频文件交给 VLC / mpv / foobar2000 / MusicBee 等播放器 - `play --url`：把网络音频 URL 交给支持 URL 播放的播放器 ### 5) 控制播放器 ```bash python3 scripts/music_skill.py control --app spotify --action play python3 scripts/music_skill.py control --app spotify --action pause python3 scripts/music_skill.py control --app spotify --action next python3 scripts/music_skill.py control --app spotify --action previous python3 scripts/music_skill.py control --app spotify --action status python3 scripts/music_skill.py control --app apple-music --action playpause python3 scripts/music_skill.py control --app apple-music --action status ``` 支持动作： - `play` - `pause` - `playpause` - `next` - `previous` - `status` ### 6) 推荐 ```bash python3 scripts/music_skill.py recommend "适合写代码的歌" python3 scripts/music_skill.py recommend "跑步音乐" -k 5 ``` ## 推荐工作流 当用户说“帮我放点适合学习的歌”时，按下面顺序执行： 1. 先运行 `scan` 2. 选一个已安装且支持搜索/播放的应用；在 macOS 上优先 Spotify / Apple Music 控制版 3. 运行 `recommend "<用户需求>"` 4. 从结果里挑最合适的 `suggestions[0].query` 5. macOS 上优先运行 `play "<query>" --app spotify --control-mode macos-ui` 6. 如果控制失败，再退回 `search "<query>" --open` ## 结果解释 典型 JSON 字段： - `app`: 选中的应用 ID - `method`: `uri` / `web` / `play_file` / `play_url` / `macos_ui_automation` - `target`: 即将打开的 URI、URL、文件路径或音频地址 - `open_result.ok`: 是否成功发起打开动作 - `supports`: 该软件支持的能力集合 - `best_effort`: 是否属于“尽力而为”的 UI 自动化 - `requires_permissions`: 控制版需要的系统权限 ## 失败时怎么处理 - 没检测到音乐软件：建议用户先安装 Spotify、Apple Music、VLC 或 mpv - macOS 控制失败：先检查“辅助功能”和“自动化”权限，再重试 - 目标软件不支持统一搜索入口：给出 query，让用户手动在应用里搜索 - 本地文件不存在：明确报错，不要猜测路径 - Linux 缺少 `xdg-open`：提示安装桌面打开器或改为手动复制 URL ## 权限说明 macOS 控制版依赖： - `osascript` - 运行该 Skill 的宿主应用已获 **辅助功能** - 运行该 Skill 的宿主应用已获 **自动化**，允许控制 Spotify / Music 没有这些权限时，控制版会失败，但扫描、推荐、普通搜索仍可用。 ## 安全边界 - 仅执行本地启动、打开 URI/URL、把用户明确提供的文件/URL交给播放器 - 在 macOS 上仅调用可审计的 AppleScript 控制公开桌面应用 - 不读取浏览器 Cookie、不抓包、不注入播放器进程 - 不使用 `curl | bash`、远程脚本直灌、Base64 混淆执行 - 所有外部入口都在脚本返回结果中可审计

path: README.md
content: # desktop-music-launcher

一个面向 OpenClaw / ClawHub 的桌面音乐技能：扫描本机音乐软件、打开它们，并基于用户需求完成推荐、搜索与播放。2.1 控制增强版新增两段式 Spotify 点播链路：优先使用可选的 Spotify Web API 精确搜索出首个匹配 track URI，再交给桌面客户端直接播放；未提供 token 时，退化到桌面快速搜索回车播放首个候选。

## 设计结论

这个 Skill 采用“本机发现 + 分层能力路由 + macOS 控制增强”的方案，而不是直接绑死某个音乐平台 API，原因有四点：

1. **规范层面更稳**  
   OpenClaw / ClawHub 当前核心要求是：技能目录中必须有 `SKILL.md`，元数据由 YAML frontmatter 提取，运行依赖应在 `metadata.openclaw` 中声明。这样更适合做本地脚本型 Skill，而不是把核心能力押注在某个第三方私有接口上。  
2. **生态层面更容易通过审核与复用**  
   公开 Skill 更适合做可审计、低耦合、少秘密依赖的实现；本 Skill 只依赖 Python 与系统默认能力，控制版仅在 macOS 额外使用系统自带 `osascript`。  
3. **工程层面覆盖更广**  
   用户电脑上的音乐软件并不统一。有人是 Spotify，有人是 Apple Music，也有人是 VLC / mpv / foobar2000。先发现、再选择、再分发动作，兼容性更高。  
4. **用户体验更强**  
   原版只能“搜/开”，控制版在 macOS 上可以直接做播放控制，并在权限到位时尽力把查询结果拉起并开始播放。

## 适用场景

- “看看我电脑里装了哪些音乐软件”
- “打开 Spotify / Apple Music / VLC”
- “帮我搜《稻香》”
- “推荐几首适合跑步的歌，然后直接开始播放”
- “暂停一下 / 下一首 / 看看现在播的是什么”
- “用 VLC 播放这个文件”

## 安装要求

基础要求：

- Python 3.9+（推荐）
- Windows / macOS / Linux 任一桌面环境
- 若需要一键打开网页/URI，系统应支持默认协议处理器
- Linux 若要打开 URL，通常需要 `xdg-open`

控制版增强要求（macOS）：

- 系统自带 `osascript`
- 已安装 Spotify 或 Music.app
- 运行 Skill 的宿主应用获得：
  - **辅助功能（Accessibility）**
  - **自动化（Automation）**

## 目录结构

```text
desktop-music-launcher/
├── SKILL.md
├── README.md
├── SELF_CHECK.md
├── scripts/
│   ├── music_skill.py
│   └── smoke_check.py
├── resources/
│   ├── music_apps.json
│   └── recommendation_profiles.json
├── examples/
│   └── usage-example.md
└── tests/
    └── smoke-test.md
```

## 核心能力

### 1. 检索音乐软件

通过常见安装路径和 PATH 命令发现：

- Spotify
- Apple Music / iTunes
- 网易云音乐
- QQ 音乐
- VLC
- mpv
- foobar2000
- MusicBee
- Rhythmbox

### 2. 打开软件

```bash
python3 scripts/music_skill.py open --app spotify
python3 scripts/music_skill.py open --app vlc
```

### 3. 推荐

```bash
python3 scripts/music_skill.py recommend "适合学习的歌"
```

输出的是“可继续搜索/播放的候选 query”，而不是伪造某个服务端推荐结果。

### 4. 搜索

```bash
python3 scripts/music_skill.py search "周杰伦 稻香" --app spotify --open
python3 scripts/music_skill.py search "陈奕迅 富士山下" --app apple-music --open
```

### 5. 播放

#### 通用模式

```bash
python3 scripts/music_skill.py play "Taylor Swift Love Story" --app spotify --open
python3 scripts/music_skill.py play --file "~/Music/demo.mp3" --app vlc --open
python3 scripts/music_skill.py play --url "https://example.com/demo.mp3" --app mpv --open
```

#### 控制版模式（macOS）

```bash
python3 scripts/music_skill.py play "Taylor Swift Love Story" --app spotify --control-mode macos-ui
python3 scripts/music_skill.py play "周杰伦 稻香" --app apple-music --control-mode macos-ui
```

说明：

- 这是 **best-effort** 的 UI 自动化，不承诺所有客户端版本和所有语言界面都完全一致
- 它依赖你已经授予宿主应用“辅助功能 + 自动化”权限
- 成功与否以脚本返回 JSON 为准，不要在代理层伪造“已开始播放”

### 6. 控制播放器

```bash
python3 scripts/music_skill.py control --app spotify --action play
python3 scripts/music_skill.py control --app spotify --action pause
python3 scripts/music_skill.py control --app spotify --action next
python3 scripts/music_skill.py control --app spotify --action previous
python3 scripts/music_skill.py control --app spotify --action status

python3 scripts/music_skill.py control --app apple-music --action playpause
python3 scripts/music_skill.py control --app apple-music --action status
```

返回示例：

```json
{
  "app": "spotify",
  "action": "status",
  "ok": true,
  "state": "playing",
  "artist": "周杰伦",
  "track": "稻香"
}
```

## 输入输出示例

### 输入

```bash
python3 scripts/music_skill.py recommend "适合睡前听的音乐"
```

### 输出

```json
{
  "input": "适合睡前听的音乐",
  "top_k": 3,
  "suggestions": [
    {
      "profile": "sleep",
      "description": "适合睡前、放松、冥想",
      "query": "sleep sounds",
      "reason": "更接近白噪音与环境音"
    }
  ],
  "hint": "可将 suggestions[0].query 继续用于 search 或 play 命令"
}
```

### 输入

```bash
python3 scripts/music_skill.py control --app spotify --action status
```

### 输出

```json
{
  "app": "spotify",
  "action": "status",
  "ok": true,
  "state": "paused",
  "artist": "Eason Chan",
  "track": "富士山下"
}
```

## 代理触发示例

- 用户：“帮我看看电脑里有哪些音乐软件。”
  - 执行：`scan`
- 用户：“打开 Spotify。”
  - 执行：`open --app spotify`
- 用户：“推荐适合写代码的歌，并帮我放。”
  - 执行：`scan` → `recommend "适合写代码的歌"` →  
    - macOS：`play "<第一候选>" --app spotify --control-mode macos-ui`  
    - 其他系统：`search "<第一候选>" --app spotify --open`
- 用户：“暂停一下。”
  - 执行：`control --app spotify --action pause`
- 用户：“用 VLC 播放这个文件。”
  - 执行：`play --file "<路径>" --app vlc --open`

## 常见问题

### 为什么“控制版”主要先支持 macOS？

因为 macOS 有系统自带的 AppleScript / `osascript` 与自动化权限模型，能把“控制播放器”做成可审计、可回退的公开实现。Windows / Linux 各客户端差异更大，后续适合做可选插件层，而不是在基础版里硬绑高脆弱方案。

### 为什么不是所有 query 都能精准点播某一首歌？

因为这版默认不绑定 Spotify OAuth、Apple Music 私有搜索接口，也不读用户账号数据。控制版的“按查询播放”是 UI 自动化，目标是提升“主动点播”的成功率，而不是伪造一个不存在的官方精确搜索 API。

### 为什么 Apple Music / Spotify 有时能打开搜索页，但没马上播？

这通常是权限、客户端状态、订阅状态、地区版权，或 UI 自动化与当前客户端版本不完全匹配导致。先确认权限，再用 `control --action status` 检查播放器当前状态。

## 风险提示

- 搜索 / 播放是否最终成功，还取决于本机软件是否正确安装、是否已登录、系统是否注册了协议处理器
- Apple Music / Spotify 等流媒体能否真正播放指定曲目，受账号订阅、地区版权与客户端状态影响
- UI 自动化属于“尽力而为”能力，可能受客户端版本、界面语言、焦点状态影响
- 本 Skill 不保证跨平台对每个音乐客户端都实现“内部精确选曲后立即开播”，但保证流程、权限和返回结果可审计


## 2.1 解决了什么

此前在 Spotify 上执行 `play "荷塘月色"` 时，旧逻辑更像是“打开搜索页后恢复上一次暂停歌曲”，而不是“从搜索结果里开始播放目标歌曲”。2.1 改成了两段式执行：

1. **优先精确点播**：若提供 `SPOTIFY_ACCESS_TOKEN` 或 `--spotify-token`，脚本会调用 Spotify Search API 找到第一首匹配 track，并用 track URI 直接交给 Spotify 播放。
2. **无 token 时尽力播放**：退化为 Spotify 桌面端快速搜索（Cmd+K）并回车播放首个候选，不再发送可能触发“恢复上一首”的空格键。

推荐命令：

```bash
export SPOTIFY_ACCESS_TOKEN="<你的 access token>"
python3 scripts/music_skill.py play "凤凰传奇 荷塘月色" --app spotify --control-mode macos-ui --spotify-market JP
```

不带 token 的退化模式：

```bash
python3 scripts/music_skill.py play "凤凰传奇 荷塘月色" --app spotify --control-mode macos-ui
```


path: SELF_CHECK.md
content: # SELF_CHECK

## 1. 规范检查

- [x] Skill 为独立文件夹
- [x] 包含 `SKILL.md`
- [x] `SKILL.md` 使用 YAML frontmatter
- [x] frontmatter 含 `name`
- [x] frontmatter 含 `description`
- [x] frontmatter 含 `version`
- [x] 运行要求写入 `metadata.openclaw`
- [x] 未声明不存在的安装器类型
- [x] 全部附属文件均为文本文件，符合发布要求
- [x] `description` 未使用易触发 YAML 解析歧义的冒号写法

## 2. 必要文件检查

- [x] `README.md`
- [x] `SELF_CHECK.md`
- [x] `scripts/music_skill.py`
- [x] `scripts/smoke_check.py`
- [x] `resources/music_apps.json`
- [x] `resources/recommendation_profiles.json`
- [x] `examples/usage-example.md`
- [x] `tests/smoke-test.md`

## 3. 脚本检查

- [x] 脚本可直接执行
- [x] 命令行参数明确
- [x] 无 TODO / 伪代码
- [x] 有错误处理
- [x] 默认只进行本地可审计操作
- [x] 不依赖未声明的第三方 Python 包
- [x] macOS 控制能力默认仅依赖系统自带 `osascript`；Spotify 精确点播为可选 token 增强

## 4. 资源引用检查

- [x] `scripts/music_skill.py` 真实读取 `resources/music_apps.json`
- [x] `scripts/music_skill.py` 真实读取 `resources/recommendation_profiles.json`

## 5. 安全审计

- [x] 未使用 `curl | bash`
- [x] 未下载远程执行脚本
- [x] 未使用 Base64 混淆执行
- [x] 未尝试读取浏览器 Cookie / token
- [x] 未逆向私有 API
- [x] 文件读写范围仅限 Skill 自身资源和用户明确提供的路径参数
- [x] 对外动作仅为：打开本地程序、打开 URI/URL、将文件或 URL 传给播放器、在 macOS 上调用 AppleScript 控制指定音乐 App

### 安全结论

该 Skill 属于**低到中风险的本地启动与桌面控制类 Skill**。  
风险主要来自“打开用户请求的 URI/URL / 文件路径”和“在 macOS 上驱动桌面自动化”这两类动作，而不是凭证、提权或隐蔽联网行为。  
控制版仍保持可审计：目标应用、动作类型、查询内容和执行结果都在 JSON 返回中可见。

## 6. 热门度与实用性评估

- 高频需求：高
- 理解门槛：低
- 传播性：高
- 二次定制空间：高
- 对特定平台/账号依赖：中
- 长期维护成本：中

## 7. 工程质量评分

| 项目 | 分数(10) | 说明 |
|---|---:|---|
| 规范对齐 | 9 | frontmatter 与目录完整 |
| 可执行性 | 9 | 脚本可直接跑 |
| 兼容性 | 8 | 多平台，多软件，macOS 控制能力更强 |
| 安全性 | 9 | 无高风险安装/执行模式 |
| 可维护性 | 9 | 数据驱动，资源与逻辑分离 |
| 实用性 | 9 | 相比原版新增主动控播 |

## 8. 已知边界

- [x] macOS 下 `control` 可直接控制 Spotify / Apple Music
- [x] macOS 下 `play <query>` 可走 UI 自动化模式
- [ ] Windows / Linux 仍以打开与搜索为主，后续可加可选控制插件层
- [x] Spotify 在提供 access token 时可先精确搜索首个匹配 track URI 再触发播放
- [ ] “按 query 精准命中某首歌并保证开播”仍受客户端版本、订阅、版权、地区与 UI 差异影响


path: scripts/music_skill.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
music_skill.py
发现本机音乐软件，启动软件，并基于已安装软件构造搜索/播放入口。
控制版增强：
- macOS 下支持 Spotify / Music.app 的 AppleScript 控制
- 支持 play/pause/next/previous/status
- 支持对查询执行“尽力开始播放”的 UI 自动化模式（需要辅助功能 + 自动化权限）
- 对 Spotify 提供可选的 Web API 精确搜索 + 轨道 URI 播放链路（需要用户自行提供 access token）
默认只做本机安全操作：扫描、启动可执行文件、打开 URI/网页、将本地文件交给播放器、
在 macOS 上调用 osascript 执行可审计的 AppleScript。
"""
from __future__ import annotations

import argparse
import json
import os
import platform
import shutil
import subprocess
import sys
import urllib.parse
import urllib.request
import urllib.error
import textwrap
from pathlib import Path
from typing import Any, Dict, List, Optional


SCRIPT_DIR = Path(__file__).resolve().parent
ROOT_DIR = SCRIPT_DIR.parent
RESOURCE_DIR = ROOT_DIR / "resources"
APPS_FILE = RESOURCE_DIR / "music_apps.json"
RECOMMEND_FILE = RESOURCE_DIR / "recommendation_profiles.json"


def load_json(path: Path) -> Dict[str, Any]:
    try:
        return json.loads(path.read_text(encoding="utf-8"))
    except FileNotFoundError:
        raise SystemExit(f"资源文件不存在: {path}")
    except json.JSONDecodeError as exc:
        raise SystemExit(f"资源文件格式错误: {path} -> {exc}")


def current_os() -> str:
    raw = platform.system().lower()
    if raw.startswith("darwin"):
        return "macos"
    if raw.startswith("windows"):
        return "windows"
    return "linux"


def expand_path(p: str) -> str:
    return os.path.expandvars(os.path.expanduser(p))


def which_all(commands: List[str]) -> List[str]:
    hits = []
    for cmd in commands:
        real = cmd.strip()
        if not real or " " in real:
            continue
        found = shutil.which(real)
        if found:
            hits.append(found)
    return sorted(set(hits))


def run_command(command: List[str], check: bool = True) -> subprocess.CompletedProcess:
    return subprocess.run(command, check=check, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)


def has_osascript() -> bool:
    return bool(shutil.which("osascript"))


def open_target(target: str, app_name: Optional[str] = None) -> Dict[str, Any]:
    os_name = current_os()
    try:
        if os_name == "windows":
            os.startfile(target)  # type: ignore[attr-defined]
        elif os_name == "macos":
            cmd = ["open"]
            if app_name:
                cmd.extend(["-a", app_name])
            cmd.append(target)
            run_command(cmd)
        else:
            opener = shutil.which("xdg-open")
            if not opener:
                raise RuntimeError("找不到 xdg-open")
            run_command([opener, target])
        return {"ok": True, "target": target, "app": app_name}
    except Exception as exc:
        return {"ok": False, "target": target, "app": app_name, "error": str(exc)}


def launch_command_string(command: str, extra_args: Optional[List[str]] = None) -> Dict[str, Any]:
    extra_args = extra_args or []
    try:
        if current_os() == "windows":
            parts = command.split()
            if len(parts) == 1 and shutil.which(parts[0]):
                subprocess.Popen([parts[0], *extra_args])
            else:
                subprocess.Popen(command if not extra_args else f'{command} {" ".join(extra_args)}', shell=True)
        else:
            parts = command.split()
            subprocess.Popen(parts + extra_args)
        return {"ok": True, "command": command, "args": extra_args}
    except Exception as exc:
        return {"ok": False, "command": command, "args": extra_args, "error": str(exc)}


def exists_any(paths: List[str]) -> List[str]:
    hits = []
    for path_str in paths:
        real = expand_path(path_str)
        if Path(real).exists():
            hits.append(real)
    return hits


def detect_apps() -> List[Dict[str, Any]]:
    os_name = current_os()
    catalog = load_json(APPS_FILE)["apps"]
    results: List[Dict[str, Any]] = []

    for app in catalog:
        platform_info = app.get("platforms", {}).get(os_name)
        if not platform_info:
            continue

        matched_paths = exists_any(platform_info.get("paths", []) + platform_info.get("app_paths", []))
        matched_bins = which_all(platform_info.get("commands", []))

        installed = bool(matched_paths or matched_bins)
        results.append({
            "id": app["id"],
            "label": app["label"],
            "os": os_name,
            "installed": installed,
            "matched_paths": matched_paths,
            "matched_bins": matched_bins,
            "supports": platform_info.get("supports", []),
            "search_uri_template": platform_info.get("search_uri_template"),
            "search_web_template": platform_info.get("search_web_template"),
        })

    return results


def choose_app(app_id: Optional[str] = None, capability: Optional[str] = None) -> Dict[str, Any]:
    apps = detect_apps()
    installed = [a for a in apps if a["installed"]]
    if app_id:
        for app in installed:
            if app["id"] == app_id:
                if capability and capability not in app.get("supports", []):
                    raise SystemExit(f"已安装 {app_id}，但不支持能力: {capability}")
                return app
        raise SystemExit(f"未找到已安装应用: {app_id}")

    if capability:
        for app in installed:
            if capability in app.get("supports", []):
                return app

    if installed:
        return installed[0]

    raise SystemExit("没有检测到已安装音乐软件。请先安装 Spotify / Apple Music / VLC / mpv 等。")


def open_app(app_id: Optional[str]) -> Dict[str, Any]:
    target = choose_app(app_id=app_id, capability="open")
    if target["matched_paths"]:
        executable = target["matched_paths"][0]
        try:
            subprocess.Popen([executable])
            return {"ok": True, "method": "path", "app": target["id"], "target": executable}
        except Exception as exc:
            return {"ok": False, "method": "path", "app": target["id"], "target": executable, "error": str(exc)}

    if target["matched_bins"]:
        cmd = Path(target["matched_bins"][0]).name
        return launch_command_string(cmd)

    if current_os() == "macos":
        label = target["label"].split(" / ")[0]
        return launch_command_string("open", ["-a", label])

    return {"ok": False, "app": target["id"], "error": "找不到可启动路径或命令"}


def format_template(template: str, query: str) -> str:
    encoded = urllib.parse.quote(query)
    return template.replace("{query}", encoded)


def search_music(query: str, app_id: Optional[str], auto_open: bool) -> Dict[str, Any]:
    try:
        target = choose_app(app_id=app_id, capability=None)
    except SystemExit:
        target = {
            "id": "browser-fallback",
            "search_web_template": "https://open.spotify.com/search/{query}",
            "supports": ["search_web"],
        }

    result: Dict[str, Any] = {"app": target["id"], "query": query, "auto_open": auto_open}

    uri_template = target.get("search_uri_template")
    web_template = target.get("search_web_template")

    if uri_template:
        uri = format_template(uri_template, query)
        result["target"] = uri
        result["method"] = "uri"
        if auto_open:
            result["open_result"] = open_target(uri)
        return result

    if web_template:
        url = format_template(web_template, query)
        result["target"] = url
        result["method"] = "web"
        if auto_open:
            result["open_result"] = open_target(url)
        return result

    result["warning"] = "该应用不支持直接搜索入口，已返回 query，请手动在应用内搜索。"
    return result


def applescript_escape(value: str) -> str:
    return value.replace("\\", "\\\\").replace('"', '\\"')


def run_osascript(script: str) -> Dict[str, Any]:
    if current_os() != "macos":
        return {"ok": False, "error": "AppleScript 仅支持 macOS"}
    if not has_osascript():
        return {"ok": False, "error": "当前系统缺少 osascript"}

    try:
        result = subprocess.run(
            ["osascript", "-e", script],
            capture_output=True,
            text=True,
            check=True,
        )
        return {
            "ok": True,
            "stdout": result.stdout.strip(),
            "stderr": result.stderr.strip(),
        }
    except subprocess.CalledProcessError as exc:
        return {
            "ok": False,
            "stdout": (exc.stdout or "").strip(),
            "stderr": (exc.stderr or "").strip(),
            "error": f"AppleScript 执行失败: {exc.returncode}",
        }


def macos_music_app_name() -> str:
    if Path("/System/Applications/Music.app").exists() or Path("/Applications/Music.app").exists():
        return "Music"
    return "iTunes"


def macos_direct_control(app_id: str, action: str) -> Dict[str, Any]:
    if current_os() != "macos":
        return {"ok": False, "app": app_id, "action": action, "error": "控制版仅在 macOS 提供 AppleScript 直控"}

    if app_id == "spotify":
        scripts = {
            "play": 'tell application "Spotify" to play',
            "pause": 'tell application "Spotify" to pause',
            "playpause": 'tell application "Spotify" to playpause',
            "next": 'tell application "Spotify" to next track',
            "previous": 'tell application "Spotify" to previous track',
            "status": textwrap.dedent("""\
                tell application "Spotify"
                  if not running then
                    return "not_running"
                  end if
                  set s to player state as text
                  set trackName to ""
                  set artistName to ""
                  try
                    set trackName to name of current track
                    set artistName to artist of current track
                  end try
                  return s & "|" & artistName & "|" & trackName
                end tell
            """),
        }
    elif app_id == "apple-music":
        app_name = macos_music_app_name()
        scripts = {
            "play": f'tell application "{app_name}" to play',
            "pause": f'tell application "{app_name}" to pause',
            "playpause": textwrap.dedent(f"""\
                tell application "{app_name}"
                  if player state is playing then
                    pause
                  else
                    play
                  end if
                end tell
            """),
            "next": f'tell application "{app_name}" to next track',
            "previous": f'tell application "{app_name}" to previous track',
            "status": textwrap.dedent(f"""\
                tell application "{app_name}"
                  if not running then
                    return "not_running"
                  end if
                  set s to player state as text
                  set trackName to ""
                  set artistName to ""
                  try
                    set trackName to name of current track
                    set artistName to artist of current track
                  end try
                  return s & "|" & artistName & "|" & trackName
                end tell
            """),
        }
    else:
        return {"ok": False, "app": app_id, "action": action, "error": "当前只支持 Spotify 和 Apple Music 的控制直连"}

    script = scripts.get(action)
    if not script:
        return {"ok": False, "app": app_id, "action": action, "error": f"不支持的动作: {action}"}

    result = run_osascript(script)
    payload = {"app": app_id, "action": action, **result}

    if action == "status" and result.get("ok"):
        raw = result.get("stdout", "")
        if raw == "not_running":
            payload["state"] = "not_running"
        else:
            state, artist, track = (raw.split("|", 2) + ["", "", ""])[:3]
            payload["state"] = state
            payload["artist"] = artist
            payload["track"] = track
    return payload


def spotify_access_token(cli_token: Optional[str] = None) -> Optional[str]:
    return cli_token or os.environ.get("SPOTIFY_ACCESS_TOKEN") or None


def spotify_search_top_track(query: str, token: str, market: Optional[str] = None) -> Dict[str, Any]:
    params = {"q": query, "type": "track", "limit": "1"}
    if market:
        params["market"] = market
    url = "https://api.spotify.com/v1/search?" + urllib.parse.urlencode(params)
    req = urllib.request.Request(url, headers={"Authorization": f"Bearer {token}"})
    try:
        with urllib.request.urlopen(req, timeout=12) as resp:
            payload = json.loads(resp.read().decode("utf-8"))
    except urllib.error.HTTPError as exc:
        body = exc.read().decode("utf-8", errors="replace")
        return {
            "ok": False,
            "query": query,
            "error": f"Spotify Search API HTTP {exc.code}",
            "details": body[:500],
        }
    except Exception as exc:
        return {"ok": False, "query": query, "error": f"Spotify Search API 请求失败: {exc}"}

    items = (((payload or {}).get("tracks") or {}).get("items") or [])
    if not items:
        return {"ok": False, "query": query, "error": "Spotify Search API 未返回匹配歌曲"}

    item = items[0]
    artists = ", ".join(a.get("name", "") for a in item.get("artists", []))
    return {
        "ok": True,
        "query": query,
        "track_name": item.get("name", ""),
        "artist_name": artists,
        "album_name": (item.get("album") or {}).get("name", ""),
        "spotify_uri": item.get("uri", ""),
        "external_url": ((item.get("external_urls") or {}).get("spotify", "")),
        "id": item.get("id", ""),
        "source": "spotify-web-api",
    }


def macos_spotify_play_track_uri(track_uri: str) -> Dict[str, Any]:
    script = textwrap.dedent(f"""\
        tell application "Spotify"
          activate
          play track "{applescript_escape(track_uri)}"
          delay 0.4
          return "playing"
        end tell
    """)
    result = run_osascript(script)
    return {"app": "spotify", "method": "spotify_track_uri", "track_uri": track_uri, **result}


def macos_spotify_quick_search_play(query: str, wait_seconds: float = 0.9) -> Dict[str, Any]:
    q = applescript_escape(query)
    script = textwrap.dedent(f"""\
        tell application "Spotify" to activate
        delay 0.5
        tell application "System Events"
          keystroke "k" using command down
          delay 0.25
          keystroke "{q}"
          delay {wait_seconds}
          key code 36
        end tell
        return "submitted_quick_search_play"
    """)
    result = run_osascript(script)
    return {
        "app": "spotify",
        "query": query,
        "method": "spotify_quick_search_enter",
        "requires_permissions": ["Accessibility", "Automation"],
        "best_effort": True,
        "warning": "未提供 Spotify access token，已使用桌面快速搜索回车播放首个候选；结果受当前客户端 UI 与排序影响。",
        **result,
    }


def macos_apple_music_ui_play_query(query: str, wait_seconds: float = 2.2) -> Dict[str, Any]:
    q = applescript_escape(query)
    app_name = macos_music_app_name()
    target = f"musics://music.apple.com/us/search?term={urllib.parse.quote(query)}"
    script = textwrap.dedent(f"""\
        open location "{target}"
        delay {wait_seconds}
        tell application "{app_name}" to activate
        delay 0.6
        tell application "System Events"
          keystroke "f" using command down
          delay 0.3
          keystroke "{q}"
          delay 0.8
          key code 36
          delay 0.6
          key code 36
        end tell
        return "submitted_music_search_play"
    """)
    result = run_osascript(script)
    return {
        "app": "apple-music",
        "query": query,
        "method": "apple_music_search_ui",
        "requires_permissions": ["Accessibility", "Automation"],
        "best_effort": True,
        **result,
    }


def macos_ui_play_query(
    app_id: str,
    query: str,
    spotify_token: Optional[str] = None,
    spotify_market: Optional[str] = None,
) -> Dict[str, Any]:
    if current_os() != "macos":
        return {"ok": False, "app": app_id, "query": query, "error": "UI 自动化仅支持 macOS"}
    if app_id not in {"spotify", "apple-music"}:
        return {"ok": False, "app": app_id, "query": query, "error": "UI 自动化目前仅支持 Spotify 和 Apple Music"}

    if app_id == "spotify":
        [REDACTED_SECRET])
        if [REDACTED_SECRET] = spotify_search_top_track(query, token, spotify_market)
            if search_result.get("ok") and search_result.get("spotify_uri"):
                play_result = macos_spotify_play_track_uri(search_result["spotify_uri"])
                merged = {
                    "app": "spotify",
                    "query": query,
                    "lookup": search_result,
                    "lookup_mode": "spotify-web-api",
                    **play_result,
                }
                return merged
            return {
                "app": "spotify",
                "query": query,
                "lookup_mode": "spotify-web-api",
                "lookup": search_result,
                "fallback_attempted": True,
                "fallback": macos_spotify_quick_search_play(query),
                "warning": "Spotify API 精确查找失败，已退化到桌面快速搜索播放。",
            }
        return macos_spotify_quick_search_play(query)

    return macos_apple_music_ui_play_query(query)


def play_music(
    query: Optional[str],
    app_id: Optional[str],
    file_path: Optional[str],
    url: Optional[str],
    auto_open: bool,
    control_mode: str = "auto",
    spotify_token: Optional[str] = None,
    spotify_market: Optional[str] = None,
) -> Dict[str, Any]:
    if file_path and url:
        raise SystemExit("--file 与 --url 不能同时使用")

    if file_path:
        target = choose_app(app_id=app_id, capability="play_file")
        real = expand_path(file_path)
        if not Path(real).exists():
            raise SystemExit(f"本地文件不存在: {real}")
        result = {"app": target["id"], "method": "play_file", "target": real, "auto_open": auto_open}
        if auto_open:
            if target["matched_paths"]:
                executable = target["matched_paths"][0]
                try:
                    subprocess.Popen([executable, real])
                    result["open_result"] = {"ok": True, "method": "path+arg", "app": target["id"], "target": real}
                except Exception as exc:
                    result["open_result"] = {"ok": False, "error": str(exc)}
            elif target["matched_bins"]:
                cmd = Path(target["matched_bins"][0]).name
                result["open_result"] = launch_command_string(cmd, [real])
        return result

    if url:
        target = choose_app(app_id=app_id, capability="play_url")
        result = {"app": target["id"], "method": "play_url", "target": url, "auto_open": auto_open}
        if auto_open:
            if target["matched_paths"]:
                executable = target["matched_paths"][0]
                try:
                    subprocess.Popen([executable, url])
                    result["open_result"] = {"ok": True, "method": "path+arg", "app": target["id"], "target": url}
                except Exception as exc:
                    result["open_result"] = {"ok": False, "error": str(exc)}
            elif target["matched_bins"]:
                cmd = Path(target["matched_bins"][0]).name
                result["open_result"] = launch_command_string(cmd, [url])
        return result

    if not query:
        raise SystemExit("未提供要播放的 query / file / url")

    target = None
    try:
        target = choose_app(app_id=app_id, capability=None)
    except SystemExit:
        pass

    if target and current_os() == "macos" and target["id"] in {"spotify", "apple-music"} and control_mode in {"auto", "macos-ui"}:
        return macos_ui_play_query(target["id"], query, spotify_token=spotify_token, spotify_market=spotify_market)

    return search_music(query=query, app_id=app_id, auto_open=auto_open)


def recommend(query: str, top_k: int = 3) -> Dict[str, Any]:
    rec = load_json(RECOMMEND_FILE)
    profiles = rec.get("profiles", {})
    matches = []

    lowered = query.lower()
    for key, profile in profiles.items():
        score = 0
        for kw in profile.get("keywords", []):
            if kw.lower() in lowered:
                score += 1
        if score > 0:
            matches.append((score, key, profile))

    matches.sort(key=lambda item: (-item[0], item[1]))

    suggestions: List[Dict[str, Any]] = []
    if matches:
        for _, key, profile in matches[:top_k]:
            for item in profile.get("suggestions", [])[:top_k]:
                suggestions.append({
                    "profile": key,
                    "description": profile.get("description", ""),
                    "query": item["query"],
                    "reason": item["reason"],
                })
    else:
        for item in rec.get("fallback", [])[:top_k]:
            suggestions.append({
                "profile": "fallback",
                "description": "未识别明确场景，给出通用候选",
                "query": item["query"],
                "reason": item["reason"],
            })

    seen = set()
    unique = []
    for item in suggestions:
        if item["query"] in seen:
            continue
        seen.add(item["query"])
        unique.append(item)

    return {
        "input": query,
        "top_k": top_k,
        "suggestions": unique[:top_k],
        "hint": "可将 suggestions[0].query 继续用于 search 或 play 命令",
    }


def control_app(app_id: str, action: str) -> Dict[str, Any]:
    return macos_direct_control(app_id, action)


def print_json(data: Dict[str, Any]) -> None:
    print(json.dumps(data, ensure_ascii=False, indent=2))


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(description="本机音乐软件技能脚本（控制增强版）")
    sub = parser.add_subparsers(dest="command", required=True)

    sub.add_parser("scan", help="扫描本机已安装音乐软件")

    p_open = sub.add_parser("open", help="打开音乐软件")
    p_open.add_argument("--app", dest="app_id", help="指定应用 ID，例如 spotify / vlc")

    p_search = sub.add_parser("search", help="在音乐软件中搜索歌曲/歌手/专辑")
    p_search.add_argument("query", help="要搜索的内容")
    p_search.add_argument("--app", dest="app_id", help="指定应用 ID")
    p_search.add_argument("--open", action="store_true", dest="auto_open", help="直接打开 URI/网页")

    p_play = sub.add_parser("play", help="播放歌曲、文件或 URL")
    p_play.add_argument("query", nargs="?", help="歌曲/歌手/关键词")
    p_play.add_argument("--app", dest="app_id", help="指定应用 ID")
    p_play.add_argument("--file", dest="file_path", help="要交给播放器播放的本地文件路径")
    p_play.add_argument("--url", dest="url", help="要交给播放器播放的网络 URL")
    p_play.add_argument("--open", action="store_true", dest="auto_open", help="直接打开结果")
    p_play.add_argument(
        "--control-mode",
        choices=["auto", "basic", "macos-ui"],
        default="auto",
        help="auto: 自动选择；basic: 只走普通打开/搜索；macos-ui: 强制使用 macOS 控制链路",
    )
    p_play.add_argument(
        "--spotify-token",
        dest="spotify_token",
        help="可选：Spotify Web API access token；未提供时，也可从环境变量 SPOTIFY_ACCESS_TOKEN 读取",
    )
    p_play.add_argument(
        "--spotify-market",
        dest="spotify_market",
        help="可选：Spotify 搜索市场，例如 JP / TW / HK / US",
    )

    p_ctrl = sub.add_parser("control", help="控制播放器状态（macOS 下支持 Spotify / Apple Music）")
    p_ctrl.add_argument("--app", required=True, dest="app_id", help="spotify / apple-music")
    p_ctrl.add_argument(
        "--action",
        required=True,
        choices=["play", "pause", "playpause", "next", "previous", "status"],
        help="控制动作",
    )

    p_recommend = sub.add_parser("recommend", help="根据场景或情绪推荐可搜索的歌单/歌曲方向")
    p_recommend.add_argument("query", help="例如：适合写代码的歌 / 跑步音乐 / 失恋听什么")
    p_recommend.add_argument("-k", "--top-k", type=int, default=3, dest="top_k", help="返回候选数量，默认 3")

    return parser


def main() -> None:
    parser = build_parser()
    args = parser.parse_args()

    if args.command == "scan":
        print_json({"os": current_os(), "apps": detect_apps(), "has_osascript": has_osascript()})
        return
    if args.command == "open":
        print_json(open_app(args.app_id))
        return
    if args.command == "search":
        print_json(search_music(args.query, args.app_id, args.auto_open))
        return
    if args.command == "play":
        print_json(play_music(
            args.query,
            args.app_id,
            args.file_path,
            args.url,
            args.auto_open,
            args.control_mode,
            args.spotify_token,
            args.spotify_market,
        ))
        return
    if args.command == "control":
        print_json(control_app(args.app_id, args.action))
        return
    if args.command == "recommend":
        print_json(recommend(args.query, args.top_k))
        return

    parser.print_help()
    raise SystemExit(1)


if __name__ == "__main__":
    main()


path: scripts/smoke_check.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import annotations

import json
import subprocess
import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent
SCRIPT = ROOT / "scripts" / "music_skill.py"

def run(*args: str):
    result = subprocess.run([sys.executable, str(SCRIPT), *args], capture_output=True, text=True, check=True)
    return json.loads(result.stdout)

def main():
    scan = run("scan")
    assert "apps" in scan and isinstance(scan["apps"], list)

    rec = run("recommend", "适合写代码的音乐", "--top-k", "2")
    assert len(rec["suggestions"]) == 2

    search = run("search", "周杰伦 稻香")
    assert "query" in search and search["query"] == "周杰伦 稻香"

    if scan.get("os") != "macos":
        ctrl = run("control", "--app", "spotify", "--action", "status")
        assert ctrl["ok"] is False

    print(json.dumps({
        "scan_apps": len(scan["apps"]),
        "recommendation_count": len(rec["suggestions"]),
        "search_method": search.get("method"),
        "has_osascript": scan.get("has_osascript"),
    }, ensure_ascii=False, indent=2))

if __name__ == "__main__":
    main()


path: examples/usage-example.md
content: # usage examples

## 1. 扫描本机音乐软件

```bash
python3 scripts/music_skill.py scan
```

## 2. 打开 Spotify

```bash
python3 scripts/music_skill.py open --app spotify
```

## 3. 搜索《稻香》

```bash
python3 scripts/music_skill.py search "周杰伦 稻香" --app spotify --open
```

## 4. 直接控播（macOS）

```bash
python3 scripts/music_skill.py control --app spotify --action play
python3 scripts/music_skill.py control --app spotify --action next
python3 scripts/music_skill.py control --app spotify --action status
```

## 5. 按需求尽力开始播放（macOS）

```bash
python3 scripts/music_skill.py play "适合写代码的 lofi" --app spotify --control-mode macos-ui
python3 scripts/music_skill.py play "周杰伦 稻香" --app apple-music --control-mode macos-ui
```

## 6. 推荐并播放

```bash
python3 scripts/music_skill.py recommend "适合跑步的歌"
python3 scripts/music_skill.py play "workout hits" --app spotify --control-mode macos-ui
```

## 7. 播放本地文件

```bash
python3 scripts/music_skill.py play --file "~/Music/demo.mp3" --app vlc --open
```

## 8. Spotify 精确点播（推荐）

```bash
export SPOTIFY_ACCESS_TOKEN="<你的 access token>"
python3 scripts/music_skill.py play "凤凰传奇 荷塘月色" --app spotify --control-mode macos-ui --spotify-market JP
```

## 9. Spotify 无 token 退化模式

```bash
python3 scripts/music_skill.py play "凤凰传奇 荷塘月色" --app spotify --control-mode macos-ui
```


path: tests/smoke-test.md
content: # smoke-test

## 目标

验证 Skill 在无第三方依赖环境下至少能完成：

- 脚本启动
- 扫描应用
- 推荐输出
- 搜索输出
- 非 macOS 环境下控制命令能安全失败
- macOS 环境下至少能解析 `control` 子命令

## 执行

```bash
python3 scripts/smoke_check.py
```

## 通过标准

输出 JSON，至少包含：

- `scan_apps`
- `recommendation_count`
- `search_method`
- `has_osascript`

## macOS 手工验证

在已授予“辅助功能 + 自动化”后，手工执行：

```bash
python3 scripts/music_skill.py control --app spotify --action status
python3 scripts/music_skill.py control --app spotify --action play
python3 scripts/music_skill.py play "周杰伦 稻香" --app spotify --control-mode macos-ui
```

观察：

- 是否返回 `ok: true`
- `status` 是否能拿到 state / artist / track
- `play <query>` 是否成功唤起搜索并开始播放


## 2.1 手工验证

```bash
python3 scripts/music_skill.py play "凤凰传奇 荷塘月色" --app spotify --control-mode macos-ui
SPOTIFY_ACCESS_TOKEN="<token>" python3 scripts/music_skill.py play "凤凰传奇 荷塘月色" --app spotify --control-mode macos-ui --spotify-market JP
```


path: resources/recommendation_profiles.json
content: {
  "profiles": {
    "focus": {
      "description": "适合专注、学习、写作或深度工作",
      "keywords": [
        "专注",
        "学习",
        "写作",
        "focus",
        "coding",
        "工作",
        "安静"
      ],
      "suggestions": [
        {
          "query": "lofi beats",
          "reason": "低干扰、节奏稳定"
        },
        {
          "query": "deep focus instrumental",
          "reason": "人声少，适合持续专注"
        },
        {
          "query": "piano study music",
          "reason": "钢琴纯音乐更柔和"
        }
      ]
    },
    "workout": {
      "description": "适合跑步、健身、力量训练",
      "keywords": [
        "跑步",
        "健身",
        "运动",
        "workout",
        "gym",
        "训练"
      ],
      "suggestions": [
        {
          "query": "workout hits",
          "reason": "高节奏，适合运动提速"
        },
        {
          "query": "electronic workout",
          "reason": "稳定强拍，利于保持输出"
        },
        {
          "query": "hip hop workout",
          "reason": "律动感强，适合力量训练"
        }
      ]
    },
    "sleep": {
      "description": "适合睡前、放松、冥想",
      "keywords": [
        "睡觉",
        "助眠",
        "放松",
        "relax",
        "sleep",
        "冥想"
      ],
      "suggestions": [
        {
          "query": "sleep sounds",
          "reason": "更接近白噪音与环境音"
        },
        {
          "query": "calm piano",
          "reason": "节奏舒缓"
        },
        {
          "query": "ambient sleep music",
          "reason": "氛围感强，动态小"
        }
      ]
    },
    "commute": {
      "description": "适合通勤、出门、轻松提神",
      "keywords": [
        "通勤",
        "出门",
        "早晨",
        "commute",
        "开车",
        "地铁"
      ],
      "suggestions": [
        {
          "query": "morning chill",
          "reason": "轻松但不沉闷"
        },
        {
          "query": "city pop",
          "reason": "适合通勤氛围"
        },
        {
          "query": "indie pop",
          "reason": "旋律友好，接受度高"
        }
      ]
    },
    "sad": {
      "description": "适合需要共情、疗愈或低能量状态",
      "keywords": [
        "伤感",
        "难过",
        "emo",
        "sad",
        "失恋",
        "治愈"
      ],
      "suggestions": [
        {
          "query": "sad acoustic",
          "reason": "情绪表达直接"
        },
        {
          "query": "healing songs",
          "reason": "更偏疗愈"
        },
        {
          "query": "雨天音乐",
          "reason": "中文语境下命中率高"
        }
      ]
    },
    "party": {
      "description": "适合聚会、社交、热闹场景",
      "keywords": [
        "聚会",
        "party",
        "热闹",
        "蹦迪",
        "朋友"
      ],
      "suggestions": [
        {
          "query": "party hits",
          "reason": "通用度高"
        },
        {
          "query": "dance pop",
          "reason": "适合多人场景"
        },
        {
          "query": "EDM festival",
          "reason": "能量感强"
        }
      ]
    }
  },
  "fallback": [
    {
      "query": "今日热歌",
      "reason": "中文用户检索常见入口"
    },
    {
      "query": "top hits",
      "reason": "英文平台通用"
    },
    {
      "query": "轻音乐",
      "reason": "无明确需求时接受度高"
    }
  ]
}

path: resources/music_apps.json
content: {
  "apps": [
    {
      "id": "spotify",
      "label": "Spotify",
      "platforms": {
        "windows": {
          "display_names": [
            "Spotify"
          ],
          "paths": [
            "%APPDATA%\\\\Spotify\\\\Spotify.exe",
            "%LOCALAPPDATA%\\\\Microsoft\\\\WindowsApps\\\\Spotify.exe",
            "%PROGRAMFILES%\\\\Spotify\\\\Spotify.exe",
            "%PROGRAMFILES(X86)%\\\\Spotify\\\\Spotify.exe"
          ],
          "commands": [
            "spotify",
            "Spotify"
          ],
          "open_uri_prefixes": [
            "spotify:"
          ],
          "search_uri_template": "spotify:search:{query}",
          "supports": [
            "open",
            "search",
            "play_uri"
          ]
        },
        "macos": {
          "app_names": [
            "Spotify.app"
          ],
          "app_paths": [
            "/Applications/Spotify.app"
          ],
          "commands": [
            "open -a Spotify"
          ],
          "open_uri_prefixes": [
            "spotify:"
          ],
          "search_uri_template": "spotify:search:{query}",
          "supports": [
            "control",
            "open",
            "play_query_control",
            "play_uri",
            "search"
          ]
        },
        "linux": {
          "commands": [
            "spotify",
            "spotify-launcher"
          ],
          "desktop_files": [
            "spotify.desktop"
          ],
          "open_uri_prefixes": [
            "spotify:"
          ],
          "search_uri_template": "spotify:search:{query}",
          "supports": [
            "open",
            "search",
            "play_uri"
          ]
        }
      }
    },
    {
      "id": "apple-music",
      "label": "Apple Music / iTunes",
      "platforms": {
        "windows": {
          "display_names": [
            "Apple Music",
            "iTunes"
          ],
          "paths": [
            "%PROGRAMFILES%\\\\Apple Music\\\\Apple Music.exe",
            "%PROGRAMFILES%\\\\iTunes\\\\iTunes.exe",
            "%PROGRAMFILES(X86)%\\\\iTunes\\\\iTunes.exe"
          ],
          "commands": [
            "AppleMusic",
            "iTunes"
          ],
          "search_web_template": "https://music.apple.com/us/search?term={query}",
          "supports": [
            "open",
            "search_web"
          ]
        },
        "macos": {
          "app_names": [
            "Music.app",
            "iTunes.app"
          ],
          "app_paths": [
            "/System/Applications/Music.app",
            "/Applications/Music.app",
            "/Applications/iTunes.app"
          ],
          "commands": [
            "open -a Music",
            "open -a iTunes"
          ],
          "search_web_template": "https://music.apple.com/us/search?term={query}",
          "supports": [
            "control",
            "open",
            "play_query_control",
            "search_web"
          ]
        }
      }
    },
    {
      "id": "netease-cloud-music",
      "label": "NetEase Cloud Music",
      "platforms": {
        "windows": {
          "display_names": [
            "网易云音乐"
          ],
          "paths": [
            "%LOCALAPPDATA%\\\\Programs\\\\cloudmusic\\\\cloudmusic.exe",
            "%PROGRAMFILES%\\\\NetEase\\\\CloudMusic\\\\cloudmusic.exe"
          ],
          "commands": [
            "cloudmusic"
          ],
          "search_web_template": "https://music.163.com/#/search/m/?s={query}&type=1",
          "supports": [
            "open",
            "search_web"
          ]
        },
        "macos": {
          "app_names": [
            "网易云音乐.app",
            "NetEaseMusic.app"
          ],
          "app_paths": [
            "/Applications/网易云音乐.app",
            "/Applications/NetEaseMusic.app"
          ],
          "commands": [
            "open -a 网易云音乐",
            "open -a NetEaseMusic"
          ],
          "search_web_template": "https://music.163.com/#/search/m/?s={query}&type=1",
          "supports": [
            "open",
            "search_web"
          ]
        }
      }
    },
    {
      "id": "qq-music",
      "label": "QQ Music",
      "platforms": {
        "windows": {
          "display_names": [
            "QQ音乐"
          ],
          "paths": [
            "%PROGRAMFILES%\\\\Tencent\\\\QQMusic\\\\QQMusic.exe",
            "%PROGRAMFILES(X86)%\\\\Tencent\\\\QQMusic\\\\QQMusic.exe"
          ],
          "commands": [
            "QQMusic"
          ],
          "search_web_template": "https://y.qq.com/n/ryqq/search?w={query}",
          "supports": [
            "open",
            "search_web"
          ]
        },
        "macos": {
          "app_names": [
            "QQ音乐.app",
            "QQMusic.app"
          ],
          "app_paths": [
            "/Applications/QQ音乐.app",
            "/Applications/QQMusic.app"
          ],
          "commands": [
            "open -a QQ音乐",
            "open -a QQMusic"
          ],
          "search_web_template": "https://y.qq.com/n/ryqq/search?w={query}",
          "supports": [
            "open",
            "search_web"
          ]
        }
      }
    },
    {
      "id": "vlc",
      "label": "VLC",
      "platforms": {
        "windows": {
          "display_names": [
            "VLC media player",
            "VLC"
          ],
          "paths": [
            "%PROGRAMFILES%\\\\VideoLAN\\\\VLC\\\\vlc.exe",
            "%PROGRAMFILES(X86)%\\\\VideoLAN\\\\VLC\\\\vlc.exe"
          ],
          "commands": [
            "vlc",
            "VLC"
          ],
          "supports": [
            "open",
            "play_file",
            "play_url"
          ]
        },
        "macos": {
          "app_names": [
            "VLC.app"
          ],
          "app_paths": [
            "/Applications/VLC.app"
          ],
          "commands": [
            "open -a VLC"
          ],
          "supports": [
            "open",
            "play_file",
            "play_url"
          ]
        },
        "linux": {
          "commands": [
            "vlc",
            "cvlc"
          ],
          "desktop_files": [
            "vlc.desktop"
          ],
          "supports": [
            "open",
            "play_file",
            "play_url"
          ]
        }
      }
    },
    {
      "id": "mpv",
      "label": "mpv",
      "platforms": {
        "windows": {
          "paths": [
            "%PROGRAMFILES%\\\\mpv\\\\mpv.exe"
          ],
          "commands": [
            "mpv"
          ],
          "supports": [
            "open",
            "play_file",
            "play_url"
          ]
        },
        "macos": {
          "app_names": [
            "mpv.app"
          ],
          "app_paths": [
            "/Applications/mpv.app"
          ],
          "commands": [
            "open -a mpv",
            "mpv"
          ],
          "supports": [
            "open",
            "play_file",
            "play_url"
          ]
        },
        "linux": {
          "commands": [
            "mpv"
          ],
          "desktop_files": [
            "mpv.desktop"
          ],
          "supports": [
            "open",
            "play_file",
            "play_url"
          ]
        }
      }
    },
    {
      "id": "foobar2000",
      "label": "foobar2000",
      "platforms": {
        "windows": {
          "paths": [
            "%PROGRAMFILES%\\\\foobar2000\\\\foobar2000.exe",
            "%PROGRAMFILES(X86)%\\\\foobar2000\\\\foobar2000.exe"
          ],
          "commands": [
            "foobar2000"
          ],
          "supports": [
            "open",
            "play_file"
          ]
        }
      }
    },
    {
      "id": "musicbee",
      "label": "MusicBee",
      "platforms": {
        "windows": {
          "paths": [
            "%PROGRAMFILES%\\\\MusicBee\\\\MusicBee.exe",
            "%PROGRAMFILES(X86)%\\\\MusicBee\\\\MusicBee.exe"
          ],
          "commands": [
            "MusicBee"
          ],
          "supports": [
            "open",
            "play_file"
          ]
        }
      }
    },
    {
      "id": "rhythmbox",
      "label": "Rhythmbox",
      "platforms": {
        "linux": {
          "commands": [
            "rhythmbox"
          ],
          "desktop_files": [
            "rhythmbox.desktop"
          ],
          "supports": [
            "open",
            "play_file",
            "play_url"
          ]
        }
      }
    }
  ]
}

Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)