mirror of
https://github.com/AynaLivePlayer/miaosic.git
synced 2026-05-09 09:04:05 +08:00
fix id3 write utf8 character failed. fix description and example
This commit is contained in:
@@ -4,11 +4,16 @@ Music Provider Repository, provide a universal interface for different music ser
|
||||
|
||||
## Command line Tool
|
||||
|
||||
please check [miaosic cmd tool](./cmd/miaosic/README.md)
|
||||
See [miaosic CLI docs](./cmd/miaosic/README.md).
|
||||
|
||||
## How to Use
|
||||
|
||||
please figure it out by yourself.
|
||||
Use the CLI commands documented in [cmd/miaosic/README.md](./cmd/miaosic/README.md).
|
||||
Typical flow:
|
||||
1. `miaosic providers`
|
||||
2. `miaosic search <provider> <keyword>`
|
||||
3. `miaosic url|download|lyric ...`
|
||||
4. `miaosic tag read|write <file> ...` for local metadata operations.
|
||||
|
||||
## Available Providers
|
||||
|
||||
|
||||
@@ -1,272 +1,188 @@
|
||||
# miaosic 命令行工具
|
||||
# miaosic CLI
|
||||
|
||||
> 注意: 改文档由deepseek生成,实例可能会有错误
|
||||
`miaosic` 是一个音乐服务命令行工具,支持搜索、信息查询、URL 解析、歌词、下载、二维码登录以及音频标签读写。
|
||||
|
||||
`miaosic` 是一个功能强大的音乐信息命令行工具,支持多种音乐平台(提供者),可以搜索音乐、获取媒体信息、获取播放URL、下载歌词以及进行二维码登录等操作。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 🎵 搜索音乐(支持分页)
|
||||
- ℹ️ 获取媒体详细信息
|
||||
- 🔗 获取媒体播放URL(支持指定音质)
|
||||
- 📜 获取歌词(支持多语言)
|
||||
- 🔑 二维码登录支持
|
||||
- 📋 列出所有支持的提供者及登录状态
|
||||
- 💾 会话管理(跨命令保存登录状态)
|
||||
|
||||
## 使用指南
|
||||
|
||||
### 基本命令结构
|
||||
## 基本用法
|
||||
|
||||
```bash
|
||||
miaosic [全局参数] <命令> [命令参数] [命令标志]
|
||||
miaosic [全局参数] <命令> [参数] [flags]
|
||||
```
|
||||
|
||||
### 全局参数
|
||||
## 全局参数
|
||||
|
||||
| 参数 | 缩写 | 描述 |
|
||||
|------------------|------|--------------------|
|
||||
| `--session-file` | `-s` | 指定会话文件路径(用于保存登录状态) |
|
||||
| `--json` | `-j` | 使用json输出 |
|
||||
| 参数 | 缩写 | 说明 |
|
||||
|---|---|---|
|
||||
| `--session-file` | `-s` | 会话文件路径,用于保存/恢复登录状态 |
|
||||
|
||||
### 命令列表
|
||||
说明:`--json/-j` 不是全局参数,只在部分命令中提供。
|
||||
|
||||
#### 1. 搜索音乐:`search`
|
||||
## 命令概览
|
||||
|
||||
搜索指定提供者的音乐。
|
||||
| 命令 | 说明 |
|
||||
|---|---|
|
||||
| `providers` | 列出所有 provider 及登录状态 |
|
||||
| `search` | 按关键词搜索 |
|
||||
| `info` | 查询媒体信息 |
|
||||
| `url` | 获取可播放 URL |
|
||||
| `quality` | 查看 provider 支持的音质 |
|
||||
| `lyric` | 获取歌词并可导出到文件 |
|
||||
| `download` | 下载媒体(可选写入标签) |
|
||||
| `qrlogin` | 二维码登录流程 |
|
||||
| `tag` | 读取/写入本地音频标签 |
|
||||
|
||||
```bash
|
||||
./miaosic search <provider> <keyword> [flags]
|
||||
```
|
||||
## 详细命令
|
||||
|
||||
**参数:**
|
||||
- `<provider>`: 音乐提供者(如 netease, qq 等)
|
||||
- `<keyword>`: 搜索关键词
|
||||
|
||||
**标志:**
|
||||
- `-p, --page`: 页码(默认:1)
|
||||
- `--page-size`: 每页结果数(默认:10)
|
||||
|
||||
**示例:**
|
||||
```bash
|
||||
./miaosic search netease "周杰伦" -p 1 --page-size 5
|
||||
```
|
||||
|
||||
**输出示例:**
|
||||
```
|
||||
Page.01 for "周杰伦"
|
||||
1. 屋顶 - 周杰伦,温岚 - 男女情歌对唱冠军全记录 - 5257138
|
||||
2. 想你就写信 (Live) - 周杰伦,李硕,张鑫 - 中国新歌声第二季 第13期 - 509781655
|
||||
3. 布拉格广场 - 蔡依林,周杰伦 - 看我72变 - 210049
|
||||
4. 默 (Live) - 李荣浩,周杰伦 - 2021中国好声音 第1期 - 1888354230
|
||||
5. 因为爱情 (Live) - 周杰伦,那英 - 中国新歌声第二季 第1期 - 490595315
|
||||
```
|
||||
|
||||
#### 2. 获取媒体信息:`info`
|
||||
|
||||
获取指定URI的媒体信息。
|
||||
|
||||
```bash
|
||||
./miaosic info <provider> <uri>
|
||||
```
|
||||
|
||||
**参数:**
|
||||
- `<provider>`: 音乐提供者
|
||||
- `<uri>`: 媒体URI(URL 或 ID)
|
||||
|
||||
**示例:**
|
||||
```bash
|
||||
./miaosic info netease 1827600686
|
||||
```
|
||||
|
||||
**输出示例:**
|
||||
```
|
||||
Title: 还是会想你
|
||||
Artist: 林达浪,h3R3
|
||||
Album: 还是会想你
|
||||
Cover https://p1.music.126.net/9FhSEQtMhP-JP3_U84YfWQ==/109951165798773745.jpg
|
||||
Provider: netease
|
||||
Identifier: 1827600686
|
||||
```
|
||||
|
||||
#### 3. 获取媒体URL:`url`
|
||||
|
||||
获取指定URI的媒体播放URL。(部分歌源可能需要登陆)
|
||||
|
||||
```bash
|
||||
miaosic url <provider> <uri> [flags]
|
||||
```
|
||||
|
||||
**参数:**
|
||||
- `<provider>`: 音乐提供者
|
||||
- `<uri>`: 媒体URI
|
||||
|
||||
**标志:**
|
||||
- `--quality`: 音质偏好(128k, 192k, 256k, 320k, hq, sq)
|
||||
|
||||
**示例:**
|
||||
```bash
|
||||
miaosic url netease 1827600686
|
||||
```
|
||||
|
||||
**输出示例:**
|
||||
```
|
||||
URL 1:
|
||||
Quality: 320k
|
||||
URL: http://example.com/audio.mp3
|
||||
Headers:
|
||||
User-Agent: Mozilla/5.0
|
||||
Referer: http://example.com
|
||||
```
|
||||
|
||||
#### 4. 获取歌词:`lyric`
|
||||
|
||||
获取指定URI的歌词。
|
||||
|
||||
```bash
|
||||
miaosic lyric <provider> <uri> [flags]
|
||||
```
|
||||
|
||||
**参数:**
|
||||
- `<provider>`: 音乐提供者
|
||||
- `<uri>`: 媒体URI
|
||||
|
||||
**标志:**
|
||||
- `-o, --output`: 指定输出文件
|
||||
- `--save`: 自动保存歌词(文件名格式:歌名_歌手名.lrc)
|
||||
|
||||
**示例:**
|
||||
```bash
|
||||
# 控制台输出歌词
|
||||
miaosic lyric netease 1827600686
|
||||
|
||||
# 保存歌词到指定文件
|
||||
miaosic lyric netease 1827600686 -o lyrics.txt
|
||||
|
||||
# 自动保存歌词
|
||||
miaosic lyric netease 1827600686 --save
|
||||
```
|
||||
|
||||
**输出示例(控制台):**
|
||||
```
|
||||
Language: zh
|
||||
-----
|
||||
[00:00.00]歌曲名:晴天
|
||||
[00:05.00]歌手:周杰伦
|
||||
...
|
||||
-----
|
||||
Language: en
|
||||
-----
|
||||
[00:00.00]Song: Qing Tian
|
||||
[00:05.00]Artist: Jay Chou
|
||||
...
|
||||
-----
|
||||
```
|
||||
|
||||
#### 5. 二维码登录:`qrlogin`
|
||||
|
||||
提供二维码登录相关操作。
|
||||
|
||||
##### 获取登录二维码:`getqrcode`
|
||||
|
||||
```bash
|
||||
miaosic qrlogin getqrcode <provider>
|
||||
```
|
||||
|
||||
**参数:**
|
||||
- `<provider>`: 支持登录的音乐提供者
|
||||
|
||||
**示例:**
|
||||
```bash
|
||||
miaosic qrlogin getqrcode netease
|
||||
```
|
||||
|
||||
**输出示例:**
|
||||
```
|
||||
Scan this QR code to login:
|
||||
[显示二维码图片]
|
||||
Key: 1234567890abcdef
|
||||
URL: https://login.example.com/qrcode?key=1234567890abcdef
|
||||
```
|
||||
|
||||
##### 验证登录:`verify`
|
||||
|
||||
```bash
|
||||
miaosic qrlogin verify <provider> <key>
|
||||
```
|
||||
|
||||
**参数:**
|
||||
- `<provider>`: 音乐提供者
|
||||
- `<key>`: 从 getqrcode 命令获取的 key
|
||||
|
||||
**示例:**
|
||||
```bash
|
||||
miaosic qrlogin verify netease 1234567890abcdef
|
||||
```
|
||||
|
||||
**输出示例:**
|
||||
```
|
||||
Login successful!
|
||||
Session: <session_string>
|
||||
```
|
||||
|
||||
#### 6. 列出提供者:`providers`
|
||||
|
||||
列出所有注册的提供者及其登录状态。
|
||||
### providers
|
||||
|
||||
```bash
|
||||
miaosic providers
|
||||
```
|
||||
|
||||
**输出示例:**
|
||||
```
|
||||
- bilibili-video: Not supported
|
||||
- kugou: Not logged in
|
||||
- kugou-instr: Not supported
|
||||
- kuwo: Not supported
|
||||
- netease: Logged in
|
||||
```
|
||||
|
||||
## 配置文件
|
||||
|
||||
使用 `--session-file` 参数指定会话文件路径,以便在不同命令之间保持登录状态:
|
||||
### search
|
||||
|
||||
```bash
|
||||
miaosic --session-file ~/.miaosic_session.json providers
|
||||
miaosic search <provider> <keyword> [flags]
|
||||
```
|
||||
|
||||
会话文件格式为 JSON:
|
||||
```json
|
||||
{
|
||||
"netease": "session_string_here",
|
||||
"qq": "another_session_string"
|
||||
}
|
||||
flags:
|
||||
- `-p, --page` 页码,默认 `1`
|
||||
- `--page-size` 每页数量,默认 `10`
|
||||
- `-j, --json` JSON 输出
|
||||
|
||||
示例:
|
||||
```bash
|
||||
miaosic search netease "周杰伦" -p 1 --page-size 5
|
||||
miaosic search qq Jay -j
|
||||
```
|
||||
|
||||
## 支持的提供者
|
||||
### info
|
||||
|
||||
| 提供者 | 搜索 | 媒体信息 | 播放URL | 歌词 | 登录 |
|
||||
|----------------|------|----------|---------|------|------|
|
||||
| netease | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| kuwo | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||
| kugou | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| bilibili-video | ✓ | ✓ | ✓ | ✓ | ✗ |
|
||||
```bash
|
||||
miaosic info <provider> <uri> [flags]
|
||||
```
|
||||
|
||||
flags:
|
||||
- `-j, --json` JSON 输出
|
||||
|
||||
## 常见问题
|
||||
示例:
|
||||
```bash
|
||||
miaosic info netease 1827600686
|
||||
miaosic info qq 004Z8Ihr0JIu5s -j
|
||||
```
|
||||
|
||||
### 如何保存登录状态?
|
||||
### url
|
||||
|
||||
使用 `--session-file` 参数指定会话文件路径:
|
||||
```bash
|
||||
miaosic url <provider> <uri> [flags]
|
||||
```
|
||||
|
||||
flags:
|
||||
- `--quality` 音质偏好,如 `128k/320k/flac/hq/sq`
|
||||
- `-j, --json` JSON 输出
|
||||
|
||||
示例:
|
||||
```bash
|
||||
miaosic url netease 1827600686 --quality 320k
|
||||
miaosic url qq 004Z8Ihr0JIu5s -j
|
||||
```
|
||||
|
||||
### quality
|
||||
|
||||
```bash
|
||||
miaosic quality <provider> [flags]
|
||||
```
|
||||
|
||||
flags:
|
||||
- `-j, --json` JSON 输出
|
||||
|
||||
示例:
|
||||
```bash
|
||||
miaosic quality netease
|
||||
```
|
||||
|
||||
### lyric
|
||||
|
||||
```bash
|
||||
miaosic lyric <provider> <uri> [flags]
|
||||
```
|
||||
|
||||
flags:
|
||||
- `-o, --output` 输出到指定文件
|
||||
- `--save` 自动按歌曲信息命名保存
|
||||
- `-j, --json` JSON 输出
|
||||
|
||||
示例:
|
||||
```bash
|
||||
miaosic lyric netease 1827600686
|
||||
miaosic lyric netease 1827600686 --save
|
||||
miaosic lyric qq 004Z8Ihr0JIu5s -o lyric.lrc
|
||||
```
|
||||
|
||||
### download
|
||||
|
||||
```bash
|
||||
miaosic download <provider> <uri> [flags]
|
||||
```
|
||||
|
||||
flags:
|
||||
- `--quality` 指定音质偏好
|
||||
- `--filename` 指定输出文件名
|
||||
- `--use-actual-ext` 当 `--filename` 后缀和实际下载到的音频后缀不一致时,自动替换为实际后缀
|
||||
- `--metadata` 下载后写入标签(标题/艺人/专辑/歌词/封面)
|
||||
|
||||
示例:
|
||||
```bash
|
||||
miaosic download netease 1827600686
|
||||
miaosic download qq 004Z8Ihr0JIu5s --quality 320k --filename song.mp3
|
||||
miaosic download kugou 3e3f9e3a4b47125e4b4558ca0bb4264a --quality flac --filename song.flac --use-actual-ext
|
||||
miaosic download netease 1827600686 --metadata
|
||||
```
|
||||
|
||||
### qrlogin
|
||||
|
||||
获取二维码:
|
||||
```bash
|
||||
miaosic qrlogin getqrcode <provider>
|
||||
```
|
||||
|
||||
验证登录:
|
||||
```bash
|
||||
miaosic qrlogin verify <provider> <key>
|
||||
```
|
||||
|
||||
示例:
|
||||
```bash
|
||||
miaosic --session-file ~/.miaosic_session.json qrlogin getqrcode netease
|
||||
miaosic --session-file ~/.miaosic_session.json qrlogin verify netease <key>
|
||||
```
|
||||
|
||||
### 为什么二维码在我的终端不显示?
|
||||
### tag
|
||||
|
||||
确保您使用的终端支持显示图片(如 iTerm2、Windows Terminal 等)。如果不支持,可以考虑使用其他终端或使用文本模式的二维码实现。
|
||||
读取标签:
|
||||
```bash
|
||||
miaosic tag read <file> [--format plain|json]
|
||||
```
|
||||
|
||||
### 如何添加新的音乐提供者?
|
||||
写入标签:
|
||||
```bash
|
||||
miaosic tag write <file> [flags]
|
||||
```
|
||||
|
||||
1. 实现 `miaosic.MediaProvider` 接口
|
||||
`tag write` flags:
|
||||
- `--title`
|
||||
- `--artist`
|
||||
- `--album`
|
||||
- `--lyrics`
|
||||
- `--lyrics-lang` (默认 `eng`)
|
||||
- `--cover` 封面图片路径
|
||||
- `--cover-type` 封面类型(默认前封面)
|
||||
|
||||
示例:
|
||||
```bash
|
||||
miaosic tag read ./data/test.mp3
|
||||
miaosic tag read ./data/test.m4a --format json
|
||||
|
||||
miaosic tag write ./data/test.flac --title "Title" --artist "Artist" --album "Album"
|
||||
miaosic tag write ./data/test.mp3 --lyrics "hello world" --lyrics-lang eng
|
||||
miaosic tag write ./data/test.m4a --cover ./data/cover.jpg --cover-type 3
|
||||
```
|
||||
|
||||
@@ -14,28 +14,30 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
writeMetadata bool
|
||||
downloadQuality string
|
||||
specifiedFilename string
|
||||
useActualExt bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
CmdDownload.Flags().BoolVar(&writeMetadata, "metadata", false, "Write metadata (tags, cover, lyrics) to the file")
|
||||
CmdDownload.Flags().StringVar(&downloadQuality, "quality", "", "Quality preference (e.g., 128k, 320k, flac)")
|
||||
CmdDownload.Flags().StringVar(&specifiedFilename, "filename", "", "Filename to use for download")
|
||||
CmdDownload.Flags().BoolVar(&useActualExt, "use-actual-ext", false, "If --filename extension mismatches detected audio extension, replace it with actual extension")
|
||||
}
|
||||
|
||||
var CmdDownload = &cobra.Command{
|
||||
Use: "download <provider> <uri>",
|
||||
Short: "Download media, with metadata and cover art",
|
||||
Long: `Downloads a media file from a provider.
|
||||
It fetches media information, URL, lyrics, and cover art.
|
||||
By default, it writes all available metadata to the downloaded file.
|
||||
Supported formats for metadata include MP3 and FLAC.`,
|
||||
Args: cobra.ExactArgs(2),
|
||||
Short: "Download media from a provider URI",
|
||||
Long: `Download a media file from provider URI, with optional quality and filename.
|
||||
When --metadata is enabled, miaosic also writes title/artist/album/lyrics/cover tags.`,
|
||||
Example: " miaosic download netease 1827600686\n miaosic download qq 004Z8Ihr0JIu5s --quality 320k --filename song.mp3\n miaosic download kugou <uri> --filename song.flac --use-actual-ext\n miaosic download netease 1827600686 --metadata",
|
||||
Args: cobra.ExactArgs(2),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// Steps 1-3: Get provider, media info, and URL (this part is unchanged)
|
||||
providerName := args[0]
|
||||
@@ -97,20 +99,28 @@ Supported formats for metadata include MP3 and FLAC.`,
|
||||
}
|
||||
|
||||
downloadedBytes := mediaData.Bytes()
|
||||
detectedExt := mimetype.Detect(downloadedBytes[:min(512, len(downloadedBytes))]).Extension()
|
||||
parsedURL, urlErr := url.Parse(mediaURL.Url)
|
||||
var ext string
|
||||
if urlErr != nil {
|
||||
ext = ""
|
||||
} else {
|
||||
ext = filepath.Ext(parsedURL.Path)
|
||||
urlExt := ""
|
||||
if urlErr == nil {
|
||||
urlExt = filepath.Ext(parsedURL.Path)
|
||||
}
|
||||
if ext == "" {
|
||||
ext = mimetype.Detect(downloadedBytes[:min(512, len(downloadedBytes))]).Extension()
|
||||
actualExt := detectedExt
|
||||
if actualExt == "" {
|
||||
actualExt = urlExt
|
||||
}
|
||||
ext := actualExt
|
||||
filename := sanitizeFilename(fmt.Sprintf("%s - %s%s", info.Artist, info.Title, ext))
|
||||
// Step 5: Save the file from the buffer
|
||||
if specifiedFilename != "" {
|
||||
filename = specifiedFilename
|
||||
if useActualExt && actualExt != "" {
|
||||
currentExt := filepath.Ext(filename)
|
||||
if !strings.EqualFold(currentExt, actualExt) {
|
||||
base := filename[:len(filename)-len(currentExt)]
|
||||
filename = base + actualExt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = os.WriteFile(filename, downloadedBytes, 0644)
|
||||
|
||||
@@ -13,8 +13,11 @@ func init() {
|
||||
|
||||
var CmdInfo = &cobra.Command{
|
||||
Use: "info <provider> <uri>",
|
||||
Short: "Get media info",
|
||||
Args: cobra.ExactArgs(2),
|
||||
Short: "Get media metadata by provider and URI",
|
||||
Long: `Resolve a provider-specific URI and print media metadata such as
|
||||
title, artist, album, cover, and identifier.`,
|
||||
Example: " miaosic info netease 1827600686\n miaosic info qq https://y.qq.com/n/ryqq/songDetail/004Z8Ihr0JIu5s -j",
|
||||
Args: cobra.ExactArgs(2),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
providerName := args[0]
|
||||
uri := args[1]
|
||||
|
||||
@@ -23,8 +23,11 @@ func init() {
|
||||
|
||||
var CmdLyric = &cobra.Command{
|
||||
Use: "lyric <provider> <uri>",
|
||||
Short: "Get media lyrics",
|
||||
Args: cobra.ExactArgs(2),
|
||||
Short: "Get lyrics for a media item",
|
||||
Long: `Fetch lyric tracks for a provider media URI.
|
||||
Lyrics can be printed, exported to JSON, or written to .lrc files.`,
|
||||
Example: " miaosic lyric netease 1827600686\n miaosic lyric netease 1827600686 --save\n miaosic lyric qq 004Z8Ihr0JIu5s -o lyric.lrc",
|
||||
Args: cobra.ExactArgs(2),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
providerName := args[0]
|
||||
uri := args[1]
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
var CmdProviders = &cobra.Command{
|
||||
Use: "providers",
|
||||
Short: "List all registered providers and login status",
|
||||
Long: "List all available providers and whether each provider is logged in or supports login.",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
providers := miaosic.ListAvailableProviders()
|
||||
|
||||
|
||||
@@ -13,12 +13,15 @@ import (
|
||||
var CmdQrlogin = &cobra.Command{
|
||||
Use: "qrlogin",
|
||||
Short: "QR code login operations",
|
||||
Long: "Manage provider login sessions using QR code flow.",
|
||||
}
|
||||
|
||||
var getqrcodeCmd = &cobra.Command{
|
||||
Use: "getqrcode <provider>",
|
||||
Short: "Get QR code for login",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Use: "getqrcode <provider>",
|
||||
Short: "Get QR code for login",
|
||||
Long: "Generate and print a login QR code, then return key/url for verification.",
|
||||
Example: " miaosic qrlogin getqrcode netease\n miaosic qrlogin getqrcode qq",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
providerName := args[0]
|
||||
|
||||
@@ -58,9 +61,11 @@ var getqrcodeCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
var verifyCmd = &cobra.Command{
|
||||
Use: "verify <provider> <key>",
|
||||
Short: "Verify QR login",
|
||||
Args: cobra.ExactArgs(2),
|
||||
Use: "verify <provider> <key>",
|
||||
Short: "Verify QR login",
|
||||
Long: "Verify a scanned QR login key and persist the provider session.",
|
||||
Example: " miaosic qrlogin verify netease <key>\n miaosic --session-file ~/.miaosic_session.json qrlogin verify qq <key>",
|
||||
Args: cobra.ExactArgs(2),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
providerName := args[0]
|
||||
key := args[1]
|
||||
|
||||
@@ -13,9 +13,11 @@ func init() {
|
||||
}
|
||||
|
||||
var CmdQuality = &cobra.Command{
|
||||
Use: "quality <provider>",
|
||||
Short: "List supported qualities for a provider",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Use: "quality <provider>",
|
||||
Short: "List supported qualities for a provider",
|
||||
Long: "List quality options supported by a provider, such as 128k, 320k, flac, hq, or sq.",
|
||||
Example: " miaosic quality netease\n miaosic quality qq -j",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
providerName := args[0]
|
||||
|
||||
|
||||
@@ -21,8 +21,11 @@ func init() {
|
||||
|
||||
var CmdSearch = &cobra.Command{
|
||||
Use: "search <provider> <keyword>",
|
||||
Short: "Search media by keyword",
|
||||
Args: cobra.MinimumNArgs(2),
|
||||
Short: "Search media by keyword from a provider",
|
||||
Long: `Search media from the specified provider using keyword text.
|
||||
Supports paging and optional JSON output.`,
|
||||
Example: " miaosic search netease \"周杰伦\"\n miaosic search qq Jay -p 2 --page-size 20 -j",
|
||||
Args: cobra.MinimumNArgs(2),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
providerName := args[0]
|
||||
keywords := args[1:]
|
||||
|
||||
163
cmd/miaosic/cmds/tag.go
Normal file
163
cmd/miaosic/cmds/tag.go
Normal file
@@ -0,0 +1,163 @@
|
||||
package cmds
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/AynaLivePlayer/miaosic/tag"
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
tagReadFormat string
|
||||
|
||||
tagWriteTitle string
|
||||
tagWriteArtist string
|
||||
tagWriteAlbum string
|
||||
tagWriteLyrics string
|
||||
tagWriteLang string
|
||||
tagWriteCover string
|
||||
tagWriteCoverTyp byte
|
||||
)
|
||||
|
||||
func init() {
|
||||
CmdTagRead.Flags().StringVar(&tagReadFormat, "format", "plain", "output format: plain or json")
|
||||
|
||||
CmdTagWrite.Flags().StringVar(&tagWriteTitle, "title", "", "title")
|
||||
CmdTagWrite.Flags().StringVar(&tagWriteArtist, "artist", "", "artist")
|
||||
CmdTagWrite.Flags().StringVar(&tagWriteAlbum, "album", "", "album")
|
||||
CmdTagWrite.Flags().StringVar(&tagWriteLyrics, "lyrics", "", "lyrics text")
|
||||
CmdTagWrite.Flags().StringVar(&tagWriteLang, "lyrics-lang", "eng", "lyrics language")
|
||||
CmdTagWrite.Flags().StringVar(&tagWriteCover, "cover", "", "cover image path")
|
||||
CmdTagWrite.Flags().Uint8Var(&tagWriteCoverTyp, "cover-type", tag.PictureTypeFrontCover, "cover picture type")
|
||||
|
||||
CmdTag.AddCommand(CmdTagRead)
|
||||
CmdTag.AddCommand(CmdTagWrite)
|
||||
}
|
||||
|
||||
var CmdTag = &cobra.Command{
|
||||
Use: "tag",
|
||||
Short: "Read or write audio metadata tags",
|
||||
Long: "Read or update tags (title, artist, album, lyrics, cover) for local audio files.",
|
||||
}
|
||||
|
||||
var CmdTagRead = &cobra.Command{
|
||||
Use: "read <file>",
|
||||
Short: "Read metadata tags from audio file",
|
||||
Long: "Read metadata tags from a local audio file and print in plain or JSON format.",
|
||||
Example: " miaosic tag read ./data/test.wav\n miaosic tag read ./data/test.mp3 --format json",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
filepath := args[0]
|
||||
f, err := os.Open(filepath)
|
||||
if err != nil {
|
||||
fmt.Printf("Error opening file: %v\n", err)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
meta, err := tag.Read(f)
|
||||
if err != nil {
|
||||
fmt.Printf("Error reading tags: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
switch strings.ToLower(tagReadFormat) {
|
||||
case "json":
|
||||
b, err := json.MarshalIndent(meta, "", " ")
|
||||
if err != nil {
|
||||
fmt.Printf("Error marshaling metadata: %v\n", err)
|
||||
return
|
||||
}
|
||||
fmt.Println(string(b))
|
||||
case "plain", "":
|
||||
printTagMetadata(meta)
|
||||
default:
|
||||
fmt.Printf("Unsupported format: %s (supported: plain, json)\n", tagReadFormat)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
var CmdTagWrite = &cobra.Command{
|
||||
Use: "write <file>",
|
||||
Short: "Write metadata tags to audio file",
|
||||
Long: `Write selected metadata fields to a local audio file.
|
||||
Existing tags are preserved unless the corresponding flag is provided.`,
|
||||
Example: " miaosic tag write ./song.mp3 --title \"Hello\" --artist \"A\"\n miaosic tag write ./song.m4a --lyrics \"line1\" --lyrics-lang eng\n miaosic tag write ./song.flac --cover ./data/cover.jpg --cover-type 3",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
filepath := args[0]
|
||||
if tagWriteTitle == "" && tagWriteArtist == "" && tagWriteAlbum == "" && tagWriteLyrics == "" && tagWriteCover == "" {
|
||||
fmt.Println("Nothing to write. Use at least one of --title/--artist/--album/--lyrics/--cover.")
|
||||
return
|
||||
}
|
||||
|
||||
meta := tag.Metadata{}
|
||||
if f, err := os.Open(filepath); err == nil {
|
||||
if existing, readErr := tag.Read(f); readErr == nil {
|
||||
meta = existing
|
||||
}
|
||||
_ = f.Close()
|
||||
}
|
||||
|
||||
if tagWriteTitle != "" {
|
||||
meta.Title = tagWriteTitle
|
||||
}
|
||||
if tagWriteArtist != "" {
|
||||
meta.Artist = tagWriteArtist
|
||||
}
|
||||
if tagWriteAlbum != "" {
|
||||
meta.Album = tagWriteAlbum
|
||||
}
|
||||
if tagWriteLyrics != "" {
|
||||
meta.Lyrics = []tag.Lyrics{{
|
||||
Lang: tagWriteLang,
|
||||
Lyrics: tagWriteLyrics,
|
||||
}}
|
||||
}
|
||||
if tagWriteCover != "" {
|
||||
coverData, err := os.ReadFile(tagWriteCover)
|
||||
if err != nil {
|
||||
fmt.Printf("Error reading cover image: %v\n", err)
|
||||
return
|
||||
}
|
||||
meta.Pictures = []tag.Picture{{
|
||||
Mimetype: mimetype.Detect(coverData).String(),
|
||||
Type: tagWriteCoverTyp,
|
||||
Description: "cover",
|
||||
Data: coverData,
|
||||
}}
|
||||
}
|
||||
|
||||
if err := tag.WriteTo(filepath, meta); err != nil {
|
||||
fmt.Printf("Error writing tags: %v\n", err)
|
||||
return
|
||||
}
|
||||
fmt.Println("Tags written successfully.")
|
||||
},
|
||||
}
|
||||
|
||||
func printTagMetadata(meta tag.Metadata) {
|
||||
fmt.Println("Format:", meta.Format)
|
||||
fmt.Println("Mimetype:", meta.Mimetype)
|
||||
fmt.Println("Title:", meta.Title)
|
||||
fmt.Println("Artist:", meta.Artist)
|
||||
fmt.Println("Album:", meta.Album)
|
||||
if len(meta.Lyrics) == 0 {
|
||||
fmt.Println("Lyrics: <none>")
|
||||
} else {
|
||||
for i, lyric := range meta.Lyrics {
|
||||
fmt.Printf("Lyrics[%d]: [%s] %s\n", i, lyric.Lang, lyric.Lyrics)
|
||||
}
|
||||
}
|
||||
if len(meta.Pictures) == 0 {
|
||||
fmt.Println("Pictures: <none>")
|
||||
} else {
|
||||
for i, pic := range meta.Pictures {
|
||||
fmt.Printf("Picture[%d]: type=%d mime=%s size=%d\n", i, pic.Type, pic.Mimetype, len(pic.Data))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,8 +14,11 @@ func init() {
|
||||
|
||||
var CmdUrl = &cobra.Command{
|
||||
Use: "url <provider> <uri>",
|
||||
Short: "Get media URLs",
|
||||
Args: cobra.ExactArgs(2),
|
||||
Short: "Resolve playable media URLs",
|
||||
Long: `Resolve one or more playable URLs from a provider media URI.
|
||||
Use --quality to request a preferred quality, and -j for JSON output.`,
|
||||
Example: " miaosic url netease 1827600686\n miaosic url qq 004Z8Ihr0JIu5s --quality 320k -j",
|
||||
Args: cobra.ExactArgs(2),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
providerName := args[0]
|
||||
uri := args[1]
|
||||
|
||||
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/AynaLivePlayer/miaosic/cmd/miaosic/cmds"
|
||||
"github.com/AynaLivePlayer/miaosic/cmd/miaosic/internal"
|
||||
_ "github.com/AynaLivePlayer/miaosic/providers/bilivideo"
|
||||
@@ -21,8 +22,10 @@ func init() {
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "miaosic",
|
||||
Short: "cmdline tool for miaosic.",
|
||||
Long: `cmdline tool for miaosic: a music searching tools`,
|
||||
Short: "CLI for searching, fetching, downloading, and tagging music",
|
||||
Long: `miaosic is a command-line client for music providers.
|
||||
It supports search, media info, URL resolving, lyrics, downloads,
|
||||
QR login, and audio metadata tag read/write operations.`,
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
if err := internal.RestoreSessions(sessionFile); err != nil {
|
||||
fmt.Printf("Error restoring sessions from file: %v\n", err)
|
||||
@@ -47,6 +50,7 @@ func init() {
|
||||
rootCmd.AddCommand(cmds.CmdLyric)
|
||||
rootCmd.AddCommand(cmds.CmdQuality)
|
||||
rootCmd.AddCommand(cmds.CmdDownload)
|
||||
rootCmd.AddCommand(cmds.CmdTag)
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
@@ -5,19 +5,22 @@ import (
|
||||
"github.com/bogem/id3v2/v2"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func setID3v2Metadata(tag *id3v2.Tag, meta Metadata) {
|
||||
tag.DeleteAllFrames()
|
||||
// Use Unicode-safe default encoding for text frames like TIT2/TPE1/TALB.
|
||||
tag.SetDefaultEncoding(id3v2.EncodingUTF8)
|
||||
tag.SetTitle(meta.Title)
|
||||
tag.SetArtist(meta.Artist)
|
||||
tag.SetAlbum(meta.Album)
|
||||
for _, lyric := range meta.Lyrics {
|
||||
lang := lyric.Lang
|
||||
if lang == "" {
|
||||
lang = "unk"
|
||||
uslf := id3v2.UnsynchronisedLyricsFrame{
|
||||
Encoding: id3v2.EncodingUTF8,
|
||||
Language: normalizeID3Language(lyric.Lang),
|
||||
Lyrics: lyric.Lyrics,
|
||||
}
|
||||
uslf := id3v2.UnsynchronisedLyricsFrame{Encoding: id3v2.EncodingUTF8, Language: lang[:min(3, len(lang))], Lyrics: lyric.Lyrics}
|
||||
tag.AddUnsynchronisedLyricsFrame(uslf)
|
||||
tag.AddUserDefinedTextFrame(id3v2.UserDefinedTextFrame{
|
||||
Encoding: id3v2.EncodingUTF8,
|
||||
@@ -31,6 +34,32 @@ func setID3v2Metadata(tag *id3v2.Tag, meta Metadata) {
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeID3Language(lang string) string {
|
||||
l := strings.ToLower(strings.TrimSpace(lang))
|
||||
switch l {
|
||||
case "", "unk", "unknown":
|
||||
return "und"
|
||||
case "zh", "chi", "zho", "zh-cn", "zh-hans", "zh-hant":
|
||||
return "zho"
|
||||
case "en":
|
||||
return "eng"
|
||||
case "ja", "jp":
|
||||
return "jpn"
|
||||
case "ko":
|
||||
return "kor"
|
||||
}
|
||||
if len(l) >= 3 {
|
||||
c := l[:3]
|
||||
for i := 0; i < 3; i++ {
|
||||
if c[i] < 'a' || c[i] > 'z' {
|
||||
return "und"
|
||||
}
|
||||
}
|
||||
return c
|
||||
}
|
||||
return "und"
|
||||
}
|
||||
|
||||
func WriteID3v2Tags(f *os.File, meta Metadata) error {
|
||||
if _, err := f.Seek(0, io.SeekStart); err != nil {
|
||||
return err
|
||||
|
||||
25
tag/writer_id3v2_test.go
Normal file
25
tag/writer_id3v2_test.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package tag
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/bogem/id3v2/v2"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSetID3v2MetadataSupportsUnicode(t *testing.T) {
|
||||
meta := Metadata{
|
||||
Title: "影子小姐",
|
||||
Artist: "封茗囧菌",
|
||||
Album: "中文专辑",
|
||||
Lyrics: []Lyrics{{Lang: "zh", Lyrics: "这是一段中文歌词"}},
|
||||
}
|
||||
|
||||
tag := id3v2.NewEmptyTag()
|
||||
setID3v2Metadata(tag, meta)
|
||||
|
||||
var buf bytes.Buffer
|
||||
_, err := tag.WriteTo(&buf)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
Reference in New Issue
Block a user