add miaosic cmd

This commit is contained in:
aynakeya
2025-06-27 04:09:51 +08:00
parent f6e4a9b576
commit 54094b7f89
9 changed files with 572 additions and 0 deletions

42
cmd/miaosic/cmds/info.go Normal file
View File

@@ -0,0 +1,42 @@
package cmds
import (
"fmt"
"github.com/AynaLivePlayer/miaosic"
"github.com/spf13/cobra"
)
var CmdInfo = &cobra.Command{
Use: "info <provider> <uri>",
Short: "Get media info",
Args: cobra.ExactArgs(2),
Run: func(cmd *cobra.Command, args []string) {
providerName := args[0]
uri := args[1]
provider, ok := miaosic.GetProvider(providerName)
if !ok {
fmt.Printf("Provider not found: %s\n", providerName)
return
}
meta, ok := provider.MatchMedia(uri)
if !ok {
fmt.Printf("URI not matched by provider: %s\n", uri)
return
}
info, err := provider.GetMediaInfo(meta)
if err != nil {
fmt.Printf("Error getting media info: %v\n", err)
return
}
fmt.Println("Title:", info.Title)
fmt.Println("Artist:", info.Artist)
fmt.Println("Album:", info.Album)
fmt.Println("Cover", info.Cover.Url)
fmt.Println("Provider:", info.Meta.Provider)
fmt.Println("Identifier:", info.Meta.Identifier)
},
}

151
cmd/miaosic/cmds/lyric.go Normal file
View File

@@ -0,0 +1,151 @@
package cmds
import (
"fmt"
"github.com/AynaLivePlayer/miaosic"
"github.com/spf13/cobra"
"os"
"path/filepath"
"strings"
)
var (
lyricOutput string
saveLyric bool
)
func init() {
CmdLyric.Flags().StringVarP(&lyricOutput, "output", "o", "", "Output lyrics to file")
CmdLyric.Flags().BoolVar(&saveLyric, "save", false, "Save lyrics to file with auto-generated name")
}
func sanitizeFilename(name string) string {
// 定义非法字符集合
invalidChars := `/\:*?"<>|`
// 替换非法字符为下划线
sanitized := strings.Map(func(r rune) rune {
if strings.ContainsRune(invalidChars, r) {
return '_'
}
return r
}, name)
// 移除首尾空格
sanitized = strings.TrimSpace(sanitized)
// 如果名称为空,返回默认值
if sanitized == "" {
return "unknown"
}
return sanitized
}
var CmdLyric = &cobra.Command{
Use: "lyric <provider> <uri>",
Short: "Get media lyrics",
Args: cobra.ExactArgs(2),
Run: func(cmd *cobra.Command, args []string) {
providerName := args[0]
uri := args[1]
provider, ok := miaosic.GetProvider(providerName)
if !ok {
fmt.Printf("Provider not found: %s\n", providerName)
return
}
meta, ok := provider.MatchMedia(uri)
if !ok {
fmt.Printf("URI not matched by provider: %s\n", uri)
return
}
lyrics, err := provider.GetMediaLyric(meta)
if err != nil {
fmt.Printf("Error getting media lyrics: %v\n", err)
return
}
if len(lyrics) == 0 {
fmt.Println("No lyrics found")
return
}
var mediaInfo miaosic.MediaInfo
if saveLyric && lyricOutput == "" {
info, err := provider.GetMediaInfo(meta)
if err != nil {
fmt.Printf("Failed to get media info for filename: %v\n", err)
return
}
mediaInfo = info
}
outputToFile := lyricOutput != "" || saveLyric
if outputToFile {
// 确定基础文件名
baseFilename := lyricOutput
if baseFilename == "" {
// 生成基于媒体信息的文件名
title := sanitizeFilename(mediaInfo.Title)
artist := sanitizeFilename(mediaInfo.Artist)
if title == "" {
title = "unknown_title"
}
if artist == "" {
artist = "unknown_artist"
}
baseFilename = fmt.Sprintf("%s_%s.lrc", title, artist)
}
if baseFilename == "" {
baseFilename = "lyrics.lrc"
}
// 处理多语言歌词
for _, lyric := range lyrics {
lang := lyric.Lang
if lang == "" {
lang = "unknown"
}
var filename string
if len(lyrics) == 1 {
filename = baseFilename
} else {
ext := filepath.Ext(baseFilename)
base := strings.TrimSuffix(baseFilename, ext)
filename = fmt.Sprintf("%s_%s%s", base, lang, ext)
}
// 写入文件
if err := os.WriteFile(filename, []byte(lyric.String()), 0644); err != nil {
fmt.Printf("Failed to write lyrics to %s: %v\n", filename, err)
} else {
fmt.Printf("Lyrics saved to: %s\n", filename)
}
}
} else {
// 输出到控制台
if len(lyrics) == 0 {
fmt.Println("No lyrics found")
return
}
for _, lyric := range lyrics {
lang := lyric.Lang
if lang == "" {
lang = "unknown"
}
fmt.Printf("Language: %s\n", lang)
fmt.Println("-----")
fmt.Println(lyric.String())
fmt.Println("-----")
}
}
},
}

View File

@@ -0,0 +1,35 @@
package cmds
import (
"fmt"
"github.com/AynaLivePlayer/miaosic"
"github.com/spf13/cobra"
)
var CmdProviders = &cobra.Command{
Use: "providers",
Short: "List all registered providers and login status",
Run: func(cmd *cobra.Command, args []string) {
providers := miaosic.ListAvailableProviders()
if len(providers) == 0 {
fmt.Println("No providers registered")
return
}
for _, providerName := range providers {
fmt.Printf(" - %s: ", providerName)
provider, _ := miaosic.GetProvider(providerName)
// 检查登录状态
if loginable, ok := provider.(miaosic.Loginable); ok {
status := "Not logged in"
if loginable.IsLogin() {
status = "Logged in"
}
fmt.Printf("%s\n", status)
} else {
fmt.Println("Not supported")
}
}
},
}

104
cmd/miaosic/cmds/qrcode.go Normal file
View File

@@ -0,0 +1,104 @@
package cmds
import (
"fmt"
"github.com/AynaLivePlayer/miaosic"
"github.com/AynaLivePlayer/miaosic/cmd/miaosic/internal"
"github.com/spf13/cobra"
"github.com/yeqown/go-qrcode/v2"
"github.com/yeqown/go-qrcode/writer/file"
"os"
)
var CmdQrlogin = &cobra.Command{
Use: "qrlogin",
Short: "QR code login operations",
}
var getqrcodeCmd = &cobra.Command{
Use: "getqrcode <provider>",
Short: "Get QR code for login",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
providerName := args[0]
provider, ok := miaosic.GetProvider(providerName)
if !ok {
fmt.Printf("Provider not found: %s\n", providerName)
return
}
loginable, ok := provider.(miaosic.Loginable)
if !ok {
fmt.Printf("Provider does not support login: %s\n", providerName)
return
}
qrSession, err := loginable.QrLogin()
if err != nil {
fmt.Printf("Error getting QR code: %v\n", err)
return
}
qrc, err := qrcode.New(qrSession.Url)
if err != nil {
fmt.Printf("Error creating QR code: %v\n", err)
return
}
w := file.New(os.Stdout)
fmt.Println("Scan this QR code to login:")
if err := qrc.Save(w); err != nil {
fmt.Printf("Error printing QR code: %v\n", err)
}
fmt.Println("Key:", qrSession.Key)
fmt.Println("URL:", qrSession.Url)
},
}
var verifyCmd = &cobra.Command{
Use: "verify <provider> <key>",
Short: "Verify QR login",
Args: cobra.ExactArgs(2),
Run: func(cmd *cobra.Command, args []string) {
providerName := args[0]
key := args[1]
provider, ok := miaosic.GetProvider(providerName)
if !ok {
fmt.Printf("Provider not found: %s\n", providerName)
return
}
loginable, ok := provider.(miaosic.Loginable)
if !ok {
fmt.Printf("Provider does not support login: %s\n", providerName)
return
}
qrSession := &miaosic.QrLoginSession{Key: key}
result, err := loginable.QrLoginVerify(qrSession)
if err != nil {
fmt.Printf("Error verifying QR login: %v\n", err)
return
}
if !result.Success {
fmt.Printf("QR login failed: %s\n", result.Message)
return
}
// 保存会话
session := loginable.SaveSession()
internal.SetSession(providerName, session)
fmt.Println("Login successful!")
fmt.Println("Session:", session)
},
}
func init() {
CmdQrlogin.AddCommand(getqrcodeCmd)
CmdQrlogin.AddCommand(verifyCmd)
}

View File

@@ -0,0 +1,57 @@
package cmds
import (
"fmt"
"github.com/AynaLivePlayer/miaosic"
"github.com/spf13/cobra"
"strings"
)
var (
searchPage int
searchPageSize int
)
func init() {
CmdSearch.Flags().IntVarP(&searchPage, "page", "p", 1, "Page number")
CmdSearch.Flags().IntVar(&searchPageSize, "page-size", 10, "Results per page")
}
var CmdSearch = &cobra.Command{
Use: "search <provider> <keyword>",
Short: "Search media by keyword",
Args: cobra.MinimumNArgs(2),
Run: func(cmd *cobra.Command, args []string) {
providerName := args[0]
keywords := args[1:]
keyword := strings.Join(keywords, " ")
provider, ok := miaosic.GetProvider(providerName)
if !ok {
fmt.Printf("Provider not found: %s\n", providerName)
return
}
results, err := provider.Search(keyword, searchPage, searchPageSize)
if err != nil {
fmt.Printf("Error searching: %v\n", err)
return
}
if len(results) == 0 {
fmt.Println("No results found")
return
}
fmt.Printf("Page.%02d for \"%s\"\n", searchPage, keyword)
for i, media := range results {
fmt.Printf("%d. %s - %s - %s - %s\n",
i+1,
media.Title,
media.Artist,
media.Album,
media.Meta.Identifier)
}
},
}

53
cmd/miaosic/cmds/url.go Normal file
View File

@@ -0,0 +1,53 @@
package cmds
import (
"fmt"
"github.com/AynaLivePlayer/miaosic"
"github.com/spf13/cobra"
)
func init() {
CmdUrl.Flags().String("quality", "", "Quality preference (128k, 192k, 256k, 320k, hq, sq)")
}
var CmdUrl = &cobra.Command{
Use: "url <provider> <uri>",
Short: "Get media URLs",
Args: cobra.ExactArgs(2),
Run: func(cmd *cobra.Command, args []string) {
providerName := args[0]
uri := args[1]
quality, _ := cmd.Flags().GetString("quality")
provider, ok := miaosic.GetProvider(providerName)
if !ok {
fmt.Printf("Provider not found: %s\n", providerName)
return
}
meta, ok := provider.MatchMedia(uri)
if !ok {
fmt.Printf("URI not matched by provider: %s\n", uri)
return
}
urls, err := provider.GetMediaUrl(meta, miaosic.Quality(quality))
if err != nil {
fmt.Printf("Error getting media URLs: %v\n", err)
return
}
for i, url := range urls {
fmt.Printf("URL %d:\n", i+1)
fmt.Printf(" Quality: %s\n", url.Quality)
fmt.Printf(" URL: %s\n", url.Url)
if len(url.Header) > 0 {
fmt.Println(" Headers:")
for k, v := range url.Header {
fmt.Printf(" %s: %s\n", k, v)
}
}
fmt.Println()
}
},
}

View File

@@ -0,0 +1,72 @@
package internal
import (
"encoding/json"
"fmt"
"github.com/AynaLivePlayer/miaosic"
"os"
"path/filepath"
)
var (
sessions = make(map[string]string)
)
func RestoreSessions(sessionFile string) error {
if sessionFile == "" {
return nil
}
data, err := os.ReadFile(sessionFile)
if err != nil {
if !os.IsNotExist(err) {
// 仅当文件存在且读取错误时打印日志
}
return err
}
err = json.Unmarshal(data, &sessions)
if err != nil {
return err
}
for providerName, session := range sessions {
provider, ok := miaosic.GetProvider(providerName)
if !ok {
continue
}
if loginable, ok := provider.(miaosic.Loginable); ok {
err = loginable.RestoreSession(session)
if err != nil {
fmt.Printf("failed to restore session for provider %s err: %s", providerName, err)
}
}
}
return nil
}
func SaveSessions(sessionFile string) error {
if sessionFile == "" {
return nil
}
data, err := json.MarshalIndent(sessions, "", " ")
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(sessionFile), 0755); err != nil {
return err
}
return os.WriteFile(sessionFile, data, 0600)
}
func GetSession(provider string) (string, bool) {
val, ok := sessions[provider]
return val, ok
}
func SetSession(provider, session string) {
sessions[provider] = session
}

52
cmd/miaosic/main.go Normal file
View File

@@ -0,0 +1,52 @@
package main
import (
"fmt"
"github.com/AynaLivePlayer/miaosic/cmd/miaosic/cmds"
"github.com/AynaLivePlayer/miaosic/cmd/miaosic/internal"
_ "github.com/AynaLivePlayer/miaosic/providers/bilivideo"
"github.com/AynaLivePlayer/miaosic/providers/kugou"
_ "github.com/AynaLivePlayer/miaosic/providers/kugou"
_ "github.com/AynaLivePlayer/miaosic/providers/kuwo"
_ "github.com/AynaLivePlayer/miaosic/providers/local"
_ "github.com/AynaLivePlayer/miaosic/providers/netease"
"github.com/spf13/cobra"
)
func init() {
kugou.UseInstrumental()
}
var rootCmd = &cobra.Command{
Use: "miaosic",
Short: "cmdline tool for miaosic.",
Long: `cmdline tool for miaosic: a music searching tools`,
PersistentPreRun: func(cmd *cobra.Command, args []string) {
if err := internal.RestoreSessions(sessionFile); err != nil {
fmt.Printf("Error restoring sessions from file: %v\n", err)
}
},
PersistentPostRun: func(cmd *cobra.Command, args []string) {
if err := internal.SaveSessions(sessionFile); err != nil {
fmt.Printf("Error saving sessions: %v\n", err)
}
},
}
var sessionFile string
func init() {
rootCmd.PersistentFlags().StringVarP(&sessionFile, "session-file", "s", "", "Session file path")
rootCmd.AddCommand(cmds.CmdProviders)
rootCmd.AddCommand(cmds.CmdSearch)
rootCmd.AddCommand(cmds.CmdQrlogin)
rootCmd.AddCommand(cmds.CmdInfo)
rootCmd.AddCommand(cmds.CmdUrl)
rootCmd.AddCommand(cmds.CmdLyric)
}
func main() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
}
}

6
go.mod
View File

@@ -15,8 +15,11 @@ require (
github.com/sahilm/fuzzy v0.1.0
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d
github.com/spf13/cast v1.5.0
github.com/spf13/cobra v1.9.1
github.com/stretchr/testify v1.8.4
github.com/tidwall/gjson v1.16.0
github.com/yeqown/go-qrcode/v2 v2.2.5
github.com/yeqown/go-qrcode/writer/file v1.0.0
golang.org/x/text v0.3.7
)
@@ -25,10 +28,13 @@ require (
github.com/andybalholm/cascadia v1.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/yeqown/reedsolomon v1.0.0 // indirect
golang.org/x/net v0.0.0-20211029224645-99673261e6eb // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)