mirror of
https://github.com/AynaLivePlayer/miaosic.git
synced 2025-12-06 13:02:48 +08:00
add miaosic cmd
This commit is contained in:
42
cmd/miaosic/cmds/info.go
Normal file
42
cmd/miaosic/cmds/info.go
Normal 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
151
cmd/miaosic/cmds/lyric.go
Normal 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("-----")
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
35
cmd/miaosic/cmds/providers.go
Normal file
35
cmd/miaosic/cmds/providers.go
Normal 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
104
cmd/miaosic/cmds/qrcode.go
Normal 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)
|
||||
}
|
||||
57
cmd/miaosic/cmds/search.go
Normal file
57
cmd/miaosic/cmds/search.go
Normal 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
53
cmd/miaosic/cmds/url.go
Normal 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()
|
||||
}
|
||||
},
|
||||
}
|
||||
72
cmd/miaosic/internal/session.go
Normal file
72
cmd/miaosic/internal/session.go
Normal 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
52
cmd/miaosic/main.go
Normal 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
6
go.mod
@@ -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
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user