From caec88b461196f56039acb67c5116b4d8376b930 Mon Sep 17 00:00:00 2001 From: aynakeya Date: Fri, 1 Sep 2023 18:51:31 -0700 Subject: [PATCH] init --- .gitignore | 1 + api.go | 50 +++++ error.go | 10 + go.mod | 28 +++ miaosic.go | 49 +++++ providers/bilibili/bilibili.go | 125 +++++++++++ providers/bilibili/bilibili_test.go | 28 +++ providers/bilibili/init.go | 9 + providers/bilivideo/bilivideo.go | 228 ++++++++++++++++++++ providers/bilivideo/bilivideo_test.go | 83 ++++++++ providers/kuwo/init.go | 7 + providers/kuwo/kuwo.go | 288 ++++++++++++++++++++++++++ providers/kuwo/kuwo_test.go | 84 ++++++++ providers/providers.go | 53 +++++ registry.go | 26 +++ 15 files changed, 1069 insertions(+) create mode 100644 .gitignore create mode 100644 api.go create mode 100644 error.go create mode 100644 go.mod create mode 100644 miaosic.go create mode 100644 providers/bilibili/bilibili.go create mode 100644 providers/bilibili/bilibili_test.go create mode 100644 providers/bilibili/init.go create mode 100644 providers/bilivideo/bilivideo.go create mode 100644 providers/bilivideo/bilivideo_test.go create mode 100644 providers/kuwo/init.go create mode 100644 providers/kuwo/kuwo.go create mode 100644 providers/kuwo/kuwo_test.go create mode 100644 providers/providers.go create mode 100644 registry.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..723ef36 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea \ No newline at end of file diff --git a/api.go b/api.go new file mode 100644 index 0000000..a43e7fa --- /dev/null +++ b/api.go @@ -0,0 +1,50 @@ +package miaosic + +//func GetPlaylist(meta *model.Meta) ([]*model.Media, error) { +// if v, ok := Providers[meta.Name]; ok { +// return v.GetPlaylist(meta) +// } +// return nil, ErrorNoSuchProvider +//} +// +//func FormatPlaylistUrl(pname, uri string) (string, error) { +// if v, ok := Providers[pname]; ok { +// return v.FormatPlaylistUrl(uri), nil +// } +// return "", ErrorNoSuchProvider +//} +// +//func MatchMedia(provider string, keyword string) *model.Media { +// if v, ok := Providers[provider]; ok { +// return v.MatchMedia(keyword) +// } +// return nil +//} +// +//func Search(provider string, keyword string) ([]*model.Media, error) { +// if v, ok := Providers[provider]; ok { +// return v.Search(keyword) +// } +// return nil, ErrorNoSuchProvider +//} +// +//func UpdateMedia(media *model.Media) error { +// if v, ok := Providers[media.Meta.(model.Meta).Name]; ok { +// return v.UpdateMedia(media) +// } +// return ErrorNoSuchProvider +//} +// +//func UpdateMediaUrl(media *model.Media) error { +// if v, ok := Providers[media.Meta.(model.Meta).Name]; ok { +// return v.UpdateMediaUrl(media) +// } +// return ErrorNoSuchProvider +//} +// +//func UpdateMediaLyric(media *model.Media) error { +// if v, ok := Providers[media.Meta.(model.Meta).Name]; ok { +// return v.UpdateMediaLyric(media) +// } +// return ErrorNoSuchProvider +//} diff --git a/error.go b/error.go new file mode 100644 index 0000000..8e6d5f4 --- /dev/null +++ b/error.go @@ -0,0 +1,10 @@ +package miaosic + +import "errors" + +var ErrNotImplemented = errors.New("not implemented") + +var ( + ErrorExternalApi = errors.New("external api error") + ErrorNoSuchProvider = errors.New("not such provider") +) diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1bfcdf5 --- /dev/null +++ b/go.mod @@ -0,0 +1,28 @@ +module miaosic + +replace github.com/aynakeya/deepcolor => ../deepcolor + +go 1.20 + +require ( + github.com/XiaoMengXinX/Music163Api-Go v0.1.30 + github.com/aynakeya/deepcolor v0.0.0-00010101000000-000000000000 + github.com/dhowden/tag v0.0.0-20230630033851-978a0926ee25 + github.com/go-resty/resty/v2 v2.7.0 + github.com/spf13/cast v1.5.0 + github.com/stretchr/testify v1.8.4 + github.com/tidwall/gjson v1.16.0 +) + +require ( + github.com/PuerkitoBio/goquery v1.7.1 // indirect + 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/pmezard/go-difflib v1.0.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect + golang.org/x/net v0.0.0-20211029224645-99673261e6eb // indirect + golang.org/x/text v0.3.7 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/miaosic.go b/miaosic.go new file mode 100644 index 0000000..124c762 --- /dev/null +++ b/miaosic.go @@ -0,0 +1,49 @@ +package miaosic + +type Picture struct { + Url string + Data []byte +} + +func (p Picture) Exists() bool { + return p.Url != "" || p.Data != nil +} + +type MediaMeta struct { + Provider string + Identifier string +} + +func (m MediaMeta) ID() string { + return m.Provider + "_" + m.Identifier +} + +type Media struct { + Title string + Artist string + Cover Picture + Album string + Lyric string + Url string + Header map[string]string + Meta MediaMeta +} + +type Playlist struct { + Title string + Medias []*Media + Meta MediaMeta +} + +type MediaProvider interface { + GetName() string + // MatchMedia returns a Media if the uri is matched, otherwise nil. + MatchMedia(uri string) *Media + // MatchPlaylist returns a Playlist if the uri is matched, otherwise nil. + MatchPlaylist(uri string) *Playlist + Search(keyword string) ([]*Media, error) + UpdatePlaylist(playlist *Playlist) error + UpdateMedia(media *Media) error + UpdateMediaUrl(media *Media) error + UpdateMediaLyric(media *Media) error +} diff --git a/providers/bilibili/bilibili.go b/providers/bilibili/bilibili.go new file mode 100644 index 0000000..3e715e2 --- /dev/null +++ b/providers/bilibili/bilibili.go @@ -0,0 +1,125 @@ +package bilibili + +import ( + "github.com/aynakeya/deepcolor" + "github.com/aynakeya/deepcolor/dphttp" + "github.com/tidwall/gjson" + "miaosic" + "miaosic/providers" + "regexp" +) + +type Bilibili struct { + providers.DeepcolorProvider + requester dphttp.IRequester + IdRegex0 *regexp.Regexp + IdRegex1 *regexp.Regexp + header map[string]string +} + +func NewBilibili(requester dphttp.IRequester) *Bilibili { + bili := &Bilibili{ + requester: requester, + IdRegex0: regexp.MustCompile("^[0-9]+"), + IdRegex1: regexp.MustCompile("^au[0-9]+"), + } + bili.initHeader() + bili.InfoFunc = bili.buildInfoApi() + bili.FileFunc = bili.buildFileApi() + bili.SearchFunc = bili.buildSearchApi() + return bili +} + +func (b *Bilibili) GetName() string { + return "bilibili" +} + +func (b *Bilibili) MatchMedia(keyword string) *miaosic.Media { + if id := b.IdRegex0.FindString(keyword); id != "" { + return &miaosic.Media{ + Meta: miaosic.MediaMeta{ + Provider: b.GetName(), + Identifier: id, + }, + } + } + if id := b.IdRegex1.FindString(keyword); id != "" { + return &miaosic.Media{ + Meta: miaosic.MediaMeta{ + Provider: b.GetName(), + Identifier: id[2:], + }, + } + } + return nil +} + +func (b *Bilibili) MatchPlaylist(keyword string) *miaosic.Playlist { + return nil +} + +func (b *Bilibili) initHeader() { + b.header = map[string]string{ + "user-agent": "BiliMusic/2.233.3", + } +} + +func (b *Bilibili) buildInfoApi() dphttp.ApiFunc[*miaosic.Media, *miaosic.Media] { + return deepcolor.CreateApiFunc( + b.requester, + func(media *miaosic.Media) (*dphttp.Request, error) { + return deepcolor.NewGetRequestWithSingleQuery("https://www.bilibili.com/audio/music-service-c/web/song/info", "sid", b.header)(media.Meta.Identifier) + }, + deepcolor.ParserGJson, + func(resp *gjson.Result, media *miaosic.Media) error { + if resp.Get("data.title").String() == "" { + return miaosic.ErrorExternalApi + } + media.Title = resp.Get("data.title").String() + media.Cover.Url = resp.Get("data.cover").String() + media.Artist = resp.Get("data.author").String() + return nil + }) +} + +func (b *Bilibili) buildFileApi() dphttp.ApiFunc[*miaosic.Media, *miaosic.Media] { + return deepcolor.CreateApiFunc( + b.requester, + func(media *miaosic.Media) (*dphttp.Request, error) { + // Assuming the endpoint and query are similar for file details + return deepcolor.NewGetRequestWithSingleQuery("https://api.bilibili.com/audio/music-service-c/url?device=phone&mid=8047632&mobi_app=iphone&platform=ios&privilege=2&quality=2", "songid", b.header)(media.Meta.Identifier) + }, + deepcolor.ParserGJson, + func(resp *gjson.Result, media *miaosic.Media) error { + if resp.Get("data.cdns.0").String() == "" { + return miaosic.ErrorExternalApi + } + media.Url = resp.Get("data.cdns.0").String() + return nil + }) +} + +func (b *Bilibili) buildSearchApi() dphttp.ApiFuncResult[string, []*miaosic.Media] { + return deepcolor.CreateApiResultFunc( + b.requester, + func(query string) (*dphttp.Request, error) { + return deepcolor.NewGetRequestWithSingleQuery("https://api.bilibili.com/audio/music-service-c/s?search_type=music&page=1&pagesize=100", "keyword", b.header)(query) + }, + deepcolor.ParserGJson, + func(resp *gjson.Result, result *[]*miaosic.Media) error { + // Assuming data contains a list of search results + for _, r := range resp.Get("data.result").Array() { + media := &miaosic.Media{ + Title: r.Get("title").String(), + Cover: miaosic.Picture{Url: r.Get("cover").String()}, + Artist: r.Get("author").String(), + Meta: miaosic.MediaMeta{ + Provider: b.GetName(), + Identifier: r.Get("id").String(), + }, + } + *result = append(*result, media) + } + return nil + }) +} diff --git a/providers/bilibili/bilibili_test.go b/providers/bilibili/bilibili_test.go new file mode 100644 index 0000000..47a8b23 --- /dev/null +++ b/providers/bilibili/bilibili_test.go @@ -0,0 +1,28 @@ +package bilibili + +import ( + "fmt" + "github.com/stretchr/testify/require" + "miaosic" + "testing" +) + +var api miaosic.MediaProvider = NewBilibili(miaosic.Requester) + +func TestBilibili_Search(t *testing.T) { + result, err := api.Search("染 reol") + require.NoError(t, err) + require.NotEmpty(t, result) +} + +func TestBilibili_GetMusic(t *testing.T) { + media := miaosic.Media{ + Meta: miaosic.MediaMeta{ + Provider: api.GetName(), + Identifier: "1560601", + }, + } + require.NoError(t, api.UpdateMedia(&media)) + require.NoError(t, api.UpdateMediaUrl(&media)) + fmt.Println(media.Url) +} diff --git a/providers/bilibili/init.go b/providers/bilibili/init.go new file mode 100644 index 0000000..8bbc4e8 --- /dev/null +++ b/providers/bilibili/init.go @@ -0,0 +1,9 @@ +package bilibili + +import ( + "miaosic" +) + +func init() { + miaosic.RegisterProvider(NewBilibili(miaosic.Requester)) +} diff --git a/providers/bilivideo/bilivideo.go b/providers/bilivideo/bilivideo.go new file mode 100644 index 0000000..c592c3d --- /dev/null +++ b/providers/bilivideo/bilivideo.go @@ -0,0 +1,228 @@ +package bilivideo + +import ( + "errors" + "github.com/aynakeya/deepcolor" + "github.com/aynakeya/deepcolor/dphttp" + "github.com/spf13/cast" + "github.com/tidwall/gjson" + "miaosic" + "miaosic/providers" + "regexp" +) + +var _ = (miaosic.MediaProvider)(&BilibiliVideo{}) + +type BilibiliVideo struct { + providers.DeepcolorProvider + requester dphttp.IRequester + BVRegex *regexp.Regexp + IdRegex *regexp.Regexp + PageRegex *regexp.Regexp + header map[string]string +} + +func NewBilibiliViedo(requester dphttp.IRequester) *BilibiliVideo { + headers := map[string]string{ + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:51.0) Gecko/20100101 Firefox/51.0", + "Referer": "https://www.bilibili.com/", + "Origin": "https://www.bilibili.com", + "Cookie": "buvid3=9A8B3564-BDA9-407F-B45F-D5C40786CA49167618infoc;", + } + pvdr := &BilibiliVideo{ + BVRegex: regexp.MustCompile("^BV[0-9A-Za-z]+"), + IdRegex: regexp.MustCompile("^BV[0-9A-Za-z]+(\\?p=[0-9]+)?"), + PageRegex: regexp.MustCompile("p=[0-9]+"), + header: headers, + } + pvdr.InfoFunc = pvdr.buildInfoApi() + //pvdr.FileFunc = buildFileApi(requester, headers) + //pvdr.LyricFunc = buildLyricApi(requester, headers) + //pvdr.PlaylistFunc = buildPlaylistApi(requester, headers) + pvdr.SearchFunc = pvdr.buildSearchApi() + return pvdr +} + +func (b *BilibiliVideo) getPage(bv string) int { + if page := b.PageRegex.FindString(bv); page != "" { + return cast.ToInt(page[2:]) + } + return 1 +} + +func (b *BilibiliVideo) getBv(bv string) string { + return b.BVRegex.FindString(bv) +} + +func (b *BilibiliVideo) GetName() string { + return "bilibili-video" +} + +func (b *BilibiliVideo) MatchMedia(keyword string) *miaosic.Media { + if id := b.IdRegex.FindString(keyword); id != "" { + return &miaosic.Media{ + Meta: miaosic.MediaMeta{ + Provider: b.GetName(), + Identifier: id, + }, + } + } + return nil +} + +func (b *BilibiliVideo) MatchPlaylist(keyword string) *miaosic.Playlist { + return nil +} + +func (b *BilibiliVideo) UpdateMediaLyric(media *miaosic.Media) error { + return nil +} + +func (b *BilibiliVideo) buildInfoApi() dphttp.ApiFunc[*miaosic.Media, *miaosic.Media] { + return deepcolor.CreateApiFunc( + b.requester, + func(params *miaosic.Media) (*dphttp.Request, error) { + return deepcolor.NewGetRequestWithSingleQuery( + "https://api.bilibili.com/x/web-interface/view/detail?&aid=&jsonp=jsonp", + "bvid", b.header, + )(params.Meta.Identifier) + }, + deepcolor.ParserGJson, + func(result *gjson.Result, media *miaosic.Media) error { + if result.Get("data.View.title").String() == "" { + return errors.New("failed to find required data") + } + media.Title = result.Get("data.View.title").String() + media.Artist = result.Get("data.View.owner.name").String() + media.Cover.Url = result.Get("data.View.pic").String() + return nil + }) +} + +//func buildFileApi(requester dphttp.IRequester, headers map[string]string) dphttp.ApiFunc[*miaosic.Media, *miaosic.Media] { +// return deepcolor.CreateApiFunc( +// requester, +// func(params *miaosic.Media) (*dphttp.Request, error) { +// return deepcolor.NewGetRequestWithSingleQuery( +// "https://api.bilibili.com/x/player/playurl?&avid=&cid=&qn=80&type=&otype=json", +// "bvid", headers, +// )(params.Meta.Identifier) +// } +//} + +func (b *BilibiliVideo) buildSearchApi() dphttp.ApiFuncResult[string, []*miaosic.Media] { + return deepcolor.CreateApiResultFunc( + b.requester, + deepcolor.NewGetRequestWithSingleQuery( + "https://api.bilibili.com/x/web-interface/search/type?search_type=video&page=1", + "keyword", b.header), + deepcolor.ParserGJson, + func(resp *gjson.Result, result *[]*miaosic.Media) error { + if resp.Get("code").String() != "0" { + return errors.New("failed to find required data") + } + r := regexp.MustCompile("]*>") + resp.Get("data.result").ForEach(func(key, value gjson.Result) bool { + *result = append(*result, &miaosic.Media{ + Title: r.ReplaceAllString(value.Get("title").String(), ""), + Cover: miaosic.Picture{Url: "https:" + value.Get("pic").String()}, + Artist: value.Get("author").String(), + Meta: miaosic.MediaMeta{ + Provider: b.GetName(), + Identifier: value.Get("bvid").String(), + }, + }) + return true + }) + return nil + }) +} + +//pvdr.cidApi = dphttp.CreateResultAPI( +//requester, +//&dphttp.ApiInfo[string, *gjson.Result, []string]{ +//Request: deepcolor.NewGetRequestWithSingleQuery( +//"https://api.bilibili.com/x/web-interface/view/detail?&aid=&jsonp=jsonp", +//"bvid", pvdr.header, +//), +//Parser: deepcolor.ParserGJson, +//Selector: func (result *gjson.Result) ([]string, error) { +//rcids := result.Get("data.View.pages.#.cid").Array() +//cids := make([]string, 0) +//if len(cids) == 0 { +//cid := result.Get("data.View.cid").String() +//if cid == "" { +//return nil, providers.ErrorExternalApi +//} +//cids = append(cids, cid) +//} else { +//for _, r := range rcids { +//cids = append(cids, r.String()) +//} +//} +//return cids, nil +//}, +//}, +//) + +//var fileApi = dphttp.CreateReceiverAPI( +// requester, +// &dphttp.ApiInfo[[]string, *gjson.Result, *model.Media]{ +// Request: deepcolor.NewGetRequestWithQuery( +// "https://api.bilibili.com/x/player/playurl?type=&otype=json&fourk=1&qn=32&avid=", +// []string{"bvid", "cid"}, pvdr.header), +// Parser: deepcolor.ParserGJson, +// Receiver: func(result *gjson.Result, container *model.Media) error { +// uri := result.Get("data.durl.0.url").String() +// if uri == "" { +// return providers.ErrorExternalApi +// } +// container.Url = uri +// header := make(map[string]string) +// _ = copier.Copy(&header, &pvdr.header) +// header["Referer"] = fmt.Sprintf("https://www.bilibili.com/video/%s", pvdr.getBv(container.Meta.(model.Meta).Id)) +// container.Header = header +// return nil +// }, +// }) +//return pvdr +//} + +// +//var BilibiliVideoAPI *BilibiliVideo +// +//func init() { +// BilibiliVideoAPI = _newBilibiliVideo() +// Providers[BilibiliVideoAPI.GetName()] = BilibiliVideoAPI +//} +// + +// +//func (b *BilibiliVideo) GetPlaylist(playlist *model.Meta) ([]*model.Media, error) { +// return nil, providers.ErrorExternalApi +//} +// +//func (b *BilibiliVideo) FormatPlaylistUrl(uri string) string { +// return "" +//} +// +//func (b *BilibiliVideo) Search(keyword string) ([]*model.Media, error) { +// return b.searchApi(keyword) +//} +// +//func (b *BilibiliVideo) UpdateMedia(media *model.Media) error { +// err := b.infoApi(b.getBv(media.Meta.(model.Meta).Id), media) +// if err != nil { +// return providers.ErrorExternalApi +// } +// return nil +//} +// +//func (b *BilibiliVideo) UpdateMediaUrl(media *model.Media) error { +// page := b.getPage(media.Meta.(model.Meta).Id) - 1 +// cids, err := b.cidApi(b.getBv(media.Meta.(model.Meta).Id)) +// if err != nil || page > len(cids) { +// return providers.ErrorExternalApi +// } +// return b.fileApi([]string{b.getBv(media.Meta.(model.Meta).Id), cids[page]}, media) +//} diff --git a/providers/bilivideo/bilivideo_test.go b/providers/bilivideo/bilivideo_test.go new file mode 100644 index 0000000..9a5963a --- /dev/null +++ b/providers/bilivideo/bilivideo_test.go @@ -0,0 +1,83 @@ +package bilivideo + +import ( + "AynaLivePlayer/core/adapter" + "AynaLivePlayer/core/model" + "github.com/stretchr/testify/assert" + "regexp" + "strings" + "testing" +) + +func TestBV_GetMusicMeta(t *testing.T) { + var api adapter.MediaProvider = BilibiliVideoAPI + + media := model.Media{ + Meta: model.Meta{ + Name: api.GetName(), + Id: "BV1434y1q71P", + }, + } + err := api.UpdateMedia(&media) + assert.Nil(t, err) + assert.Equal(t, "卦者那啥子靈風", media.Artist) +} + +func TestBV_GetMusic(t *testing.T) { + var api adapter.MediaProvider = BilibiliVideoAPI + media := model.Media{ + Meta: model.Meta{ + Name: api.GetName(), + Id: "BV1434y1q71P", + }, + } + err := api.UpdateMedia(&media) + assert.Nil(t, err) + err = api.UpdateMediaUrl(&media) + assert.Nil(t, err) + assert.True(t, strings.Contains(media.Url, "bilivideo"), media.Url) +} + +func TestBV_Regex(t *testing.T) { + assert.Equal(t, "BV1gA411P7ir?p=3", regexp.MustCompile("^BV[0-9A-Za-z]+(\\?p=[0-9]+)?").FindString("BV1gA411P7ir?p=3")) +} + +func TestBV_GetMusicMeta2(t *testing.T) { + var api adapter.MediaProvider = BilibiliVideoAPI + + media := model.Media{ + Meta: model.Meta{ + Name: api.GetName(), + Id: "BV1gA411P7ir?p=3", + }, + } + err := api.UpdateMedia(&media) + assert.Nil(t, err) + if err != nil { + return + } + assert.Equal(t, "沈默沈默", media.Artist) +} + +func TestBV_GetMusic2(t *testing.T) { + var api adapter.MediaProvider = BilibiliVideoAPI + media := model.Media{ + Meta: model.Meta{ + Name: api.GetName(), + Id: "BV1gA411P7ir?p=1", + }, + } + err := api.UpdateMedia(&media) + assert.Nil(t, err) + err = api.UpdateMediaUrl(&media) + assert.Nil(t, err) + assert.Equal(t, "沈默沈默", media.Artist) +} + +func TestBV_Search(t *testing.T) { + var api adapter.MediaProvider = BilibiliVideoAPI + result, err := api.Search("家有女友") + assert.Nil(t, err, "Search Error") + assert.Truef(t, len(result) > 0, "Search Result Empty") + t.Log(result[0]) +} diff --git a/providers/kuwo/init.go b/providers/kuwo/init.go new file mode 100644 index 0000000..2ddd855 --- /dev/null +++ b/providers/kuwo/init.go @@ -0,0 +1,7 @@ +package kuwo + +import "miaosic" + +func init() { + miaosic.RegisterProvider(NewKuwo(miaosic.Requester)) +} diff --git a/providers/kuwo/kuwo.go b/providers/kuwo/kuwo.go new file mode 100644 index 0000000..b30011d --- /dev/null +++ b/providers/kuwo/kuwo.go @@ -0,0 +1,288 @@ +package kuwo + +import ( + "fmt" + "github.com/aynakeya/deepcolor" + "github.com/aynakeya/deepcolor/dphttp" + "github.com/spf13/cast" + "github.com/tidwall/gjson" + "html" + "math" + "math/rand" + "miaosic" + "miaosic/providers" + "regexp" + "strconv" + "strings" +) + +type Kuwo struct { + providers.DeepcolorProvider + requester dphttp.IRequester + PlaylistRegex0 *regexp.Regexp + PlaylistRegex1 *regexp.Regexp + IdRegex0 *regexp.Regexp + IdRegex1 *regexp.Regexp + header map[string]string +} + +func NewKuwo(requester dphttp.IRequester) *Kuwo { + kw := &Kuwo{ + requester: requester, + PlaylistRegex0: regexp.MustCompile("[0-9]+"), + PlaylistRegex1: regexp.MustCompile("playlist/[0-9]+"), + IdRegex0: regexp.MustCompile("^[0-9]+"), + IdRegex1: regexp.MustCompile("^kw[0-9]+"), + } + kw.initToken() + kw.InfoFunc = kw.buildInfoApi() + kw.FileFunc = kw.buildFileApi() + kw.LyricFunc = kw.buildLyricApi() + kw.PlaylistFunc = kw.playlistApi + kw.SearchFunc = kw.buildSearchApi() + return kw +} + +func (k *Kuwo) GetName() string { + return "kuwo" +} + +func (k *Kuwo) MatchMedia(keyword string) *miaosic.Media { + if id := k.IdRegex0.FindString(keyword); id != "" { + return &miaosic.Media{ + Meta: miaosic.MediaMeta{ + Provider: k.GetName(), + Identifier: id, + }, + } + } + if id := k.IdRegex1.FindString(keyword); id != "" { + return &miaosic.Media{ + Meta: miaosic.MediaMeta{ + Provider: k.GetName(), + Identifier: id[2:], + }, + } + } + return nil +} + +func (k *Kuwo) MatchPlaylist(uri string) *miaosic.Playlist { + var id string + id = k.PlaylistRegex0.FindString(uri) + if id != "" { + return &miaosic.Playlist{ + Meta: miaosic.MediaMeta{k.GetName(), id}, + } + } + id = k.PlaylistRegex1.FindString(uri) + if id != "" { + return &miaosic.Playlist{ + Meta: miaosic.MediaMeta{k.GetName(), id[9:]}, + } + } + return nil +} + +func (k *Kuwo) generateSecret(t, e string) string { + if e == "" { + return "" + } + + var n string + for i := 0; i < len(e); i++ { + n += strconv.Itoa(int(e[i])) + } + r := len(n) / 5 + o_0 := string(n[r]) + string(n[2*r]) + string(n[3*r]) + string(n[4*r]) + if 5*r < len(n) { + o_0 += string(n[5*r]) + } + o, _ := strconv.Atoi(o_0) + l := int(math.Ceil(float64(len(e)) / 2.0)) + c := int(math.Pow(2, 31)) - 1 + + if o < 2 { + return "" + } + + d := rand.Intn(100000000) + //d := 80378195 + n += strconv.Itoa(d) + + var num1, num2 int64 + for len(n) > 10 { + // stupid javascript + if len(n[10:]) > 19 { + num1 = 0 + num2 = cast.ToInt64(n[19 : 19+8]) + } else { + num1 = cast.ToInt64(n[:10]) + num2 = cast.ToInt64(n[10:]) + } + n = cast.ToString(num1 + num2) + } + + nValue, _ := strconv.Atoi(n) + nValue = (o*nValue + l) % c + + var h int + var f string + + for i := 0; i < len(t); i++ { + h = int(t[i]) ^ int(math.Floor(float64(nValue)/float64(c)*255)) + hexValue := strconv.FormatInt(int64(h), 16) + + if h < 16 { + f += "0" + hexValue + } else { + f += hexValue + } + + nValue = (o*nValue + l) % c + } + + // d to hex string, if d length < 8, add 0 to head + dHex := fmt.Sprintf("%x", d) + if len(dHex) < 8 { + dHex = strings.Repeat("0", 8-len(dHex)) + dHex + } + + return f + dHex +} + +func (k *Kuwo) initToken() { + k.header = map[string]string{ + "cookie": "Hm_Iuvt_cdb524f42f0cer9b268e4v7y734w5esq24=TN7FsbxFGt8y2sTb4tGnzhpD7StNfiRM", + "secret": k.generateSecret("TN7FsbxFGt8y2sTb4tGnzhpD7StNfiRM", "Hm_Iuvt_cdb524f42f0cer9b268e4v7y734w5esq24"), + "referer": "http://www.kuwo.cn/", + } + //searchCookie, err := k.requester.Get("http://kuwo.cn/search/list?key=any", nil) + //fmt.Println(searchCookie.Header(), err) +} + +func (k *Kuwo) buildInfoApi() dphttp.ApiFunc[*miaosic.Media, *miaosic.Media] { + return deepcolor.CreateApiFunc( + k.requester, + func(media *miaosic.Media) (*dphttp.Request, error) { + return deepcolor.NewGetRequestWithQuery( + "http://www.kuwo.cn/api/www/music/musicInfo", + []string{"mid"}, k.header)([]string{media.Meta.Identifier}) + }, + deepcolor.ParserGJson, + func(resp *gjson.Result, media *miaosic.Media) error { + if resp.Get("data.musicrid").String() == "" { + return miaosic.ErrorExternalApi + } + media.Title = html.UnescapeString(resp.Get("data.name").String()) + media.Cover.Url = resp.Get("data.pic").String() + media.Artist = resp.Get("data.artist").String() + media.Album = resp.Get("data.album").String() + return nil + }) +} + +func (k *Kuwo) buildLyricApi() dphttp.ApiFunc[*miaosic.Media, *miaosic.Media] { + return deepcolor.CreateApiFunc( + k.requester, + func(media *miaosic.Media) (*dphttp.Request, error) { + return deepcolor.NewGetRequestWithQuery( + "http://m.kuwo.cn/newh5/singles/songinfoandlrc", + []string{"musicId"}, k.header)([]string{media.Meta.Identifier}) + }, + deepcolor.ParserGJson, + func(resp *gjson.Result, media *miaosic.Media) error { + lrcs := make([]string, 0) + resp.Get("data.lrclist").ForEach(func(key, value gjson.Result) bool { + lrcs = append(lrcs, fmt.Sprintf("[00:%s]%s", value.Get("time").String(), value.Get("lineLyric").String())) + + return true + }) + media.Lyric = strings.Join(lrcs, "\n") + return nil + }) +} + +func (k *Kuwo) buildSearchApi() dphttp.ApiFuncResult[string, []*miaosic.Media] { + return deepcolor.CreateApiResultFunc( + k.requester, + func(keyword string) (*dphttp.Request, error) { + return deepcolor.NewGetRequestWithQuery( + "http://www.kuwo.cn/api/www/search/searchMusicBykeyWord", + []string{"key", "pn", "rn"}, k.header)([]string{keyword, "1", "64"}) + }, + deepcolor.ParserGJson, + func(resp *gjson.Result, result *[]*miaosic.Media) error { + resp.Get("data.list").ForEach(func(key, value gjson.Result) bool { + *result = append(*result, &miaosic.Media{ + Title: html.UnescapeString(value.Get("name").String()), + Cover: miaosic.Picture{Url: value.Get("pic").String()}, + Artist: value.Get("artist").String(), + Album: value.Get("album").String(), + Meta: miaosic.MediaMeta{ + Provider: k.GetName(), + Identifier: value.Get("rid").String(), + }, + }) + return true + }) + return nil + }) +} + +func (k *Kuwo) buildFileApi() dphttp.ApiFunc[*miaosic.Media, *miaosic.Media] { + return deepcolor.CreateApiFunc( + k.requester, + func(media *miaosic.Media) (*dphttp.Request, error) { + return deepcolor.NewGetRequestWithSingleQuery( + "http://antiserver.kuwo.cn/anti.s?type=convert_url&format=mp3&response=url", + "rid", k.header)("MUSIC_" + media.Meta.Identifier) + }, + deepcolor.ParserText, + func(resp string, media *miaosic.Media) error { + media.Url = resp + return nil + }) +} + +func (k *Kuwo) playlistApi(src *miaosic.Playlist, dst *miaosic.Playlist) error { + dst.Medias = make([]*miaosic.Media, 0) + api := deepcolor.CreateChainApiFunc( + k.requester, + func(page int) (*dphttp.Request, error) { + return deepcolor.NewGetRequestWithQuery( + "http://www.kuwo.cn/api/www/playlist/playListInfo", + []string{"pid", "pn", "rn"}, k.header)([]string{src.Meta.Identifier, cast.ToString(page), "100"}) + }, + deepcolor.ParserGJson, + func(resp *gjson.Result, playlist *miaosic.Playlist) error { + resp.Get("data.musicList").ForEach(func(key, value gjson.Result) bool { + playlist.Medias = append( + playlist.Medias, + &miaosic.Media{ + Title: html.UnescapeString(value.Get("name").String()), + Artist: value.Get("artist").String(), + Cover: miaosic.Picture{Url: value.Get("pic").String()}, + Album: value.Get("album").String(), + Meta: miaosic.MediaMeta{ + Provider: k.GetName(), + Identifier: value.Get("rid").String(), + }, + }) + return true + }) + return nil + }, + func(page int, resp *gjson.Result, playlist *miaosic.Playlist) (int, bool) { + if resp.Get("code").String() != "200" { + return page, false + } + cnt := int(resp.Get("data.total").Int()) + if cnt <= page*100 { + return page, false + } + return page + 1, true + }, + ) + return api(1, dst) +} diff --git a/providers/kuwo/kuwo_test.go b/providers/kuwo/kuwo_test.go new file mode 100644 index 0000000..c883047 --- /dev/null +++ b/providers/kuwo/kuwo_test.go @@ -0,0 +1,84 @@ +package kuwo + +import ( + "fmt" + "github.com/aynakeya/deepcolor" + "github.com/stretchr/testify/require" + "miaosic" + "testing" +) + +var api miaosic.MediaProvider = NewKuwo(deepcolor.NewRestyRequester()) + +func TestKuwo_Secret(t *testing.T) { + // using 80378195 as d + require.Equal(t, "5add7ba59bc95d3d38a8983a82af6efc78d3484c6d253f29a2154dd042b3383604ca7953", + api.(*Kuwo).generateSecret("zddnb2yWCXJjk6aWb2tSZBNeaPBChEPY", "Hm_Iuvt_cdb524f42f0cer9b268e4v7y734w5esq24")) + +} + +func TestKuwo_Search(t *testing.T) { + result, err := api.Search("周杰伦") + require.NoError(t, err) + fmt.Println(result) + media := result[0] + err = api.UpdateMediaUrl(media) + fmt.Println(err) + fmt.Println(media.Url) +} + +func TestKuwo_GetMusicMeta(t *testing.T) { + media := miaosic.Media{ + Meta: miaosic.MediaMeta{ + Provider: api.GetName(), + Identifier: "22804772", + }, + } + err := api.UpdateMedia(&media) + require.NoError(t, err) + require.Equal(t, "霜雪千年", media.Title) +} + +func TestKuwo_GetMusic(t *testing.T) { + media := miaosic.Media{ + Meta: miaosic.MediaMeta{ + Provider: api.GetName(), + Identifier: "22804772", + }, + } + require.NoError(t, api.UpdateMedia(&media)) + require.NoError(t, api.UpdateMediaUrl(&media)) + require.Equal(t, "霜雪千年", media.Title) + require.True(t, len(media.Url) > 0) +} + +//func TestKuwo_UpdateMediaLyric(t *testing.T) { +// var api adapter.MediaProvider = providers.KuwoAPI +// media := model.Media{ +// Meta: model.Meta{ +// Name: api.GetName(), +// Id: "22804772", +// }, +// } +// err := api.UpdateMediaLyric(&media) +// fmt.Println(err) +// fmt.Println(media.Lyric) +//} +// +//func TestKuwo_GetPlaylist(t *testing.T) { +// var api adapter.MediaProvider = providers.KuwoAPI +// playlist, err := api.GetPlaylist(&model.Meta{ +// Name: api.GetName(), +// //Id: "1082685104", +// Id: "2959147566", +// }) +// if err != nil { +// fmt.Println(err) +// return +// } +// fmt.Println(len(playlist)) +// for _, media := range playlist { +// fmt.Println(media.Title, media.Artist, media.Album) +// } +// +//} diff --git a/providers/providers.go b/providers/providers.go new file mode 100644 index 0000000..c26c495 --- /dev/null +++ b/providers/providers.go @@ -0,0 +1,53 @@ +package providers + +import ( + "github.com/aynakeya/deepcolor/dphttp" + "miaosic" +) + +type DeepcolorProvider struct { + InfoFunc dphttp.ApiFunc[*miaosic.Media, *miaosic.Media] + FileFunc dphttp.ApiFunc[*miaosic.Media, *miaosic.Media] + LyricFunc dphttp.ApiFunc[*miaosic.Media, *miaosic.Media] + PlaylistFunc dphttp.ApiFunc[*miaosic.Playlist, *miaosic.Playlist] + SearchFunc dphttp.ApiFuncResult[string, []*miaosic.Media] +} + +func (d *DeepcolorProvider) UpdatePlaylist(playlist *miaosic.Playlist) error { + if d.PlaylistFunc == nil { + return miaosic.ErrNotImplemented + } + return d.PlaylistFunc(playlist, playlist) +} + +func (d *DeepcolorProvider) Search(keyword string) ([]*miaosic.Media, error) { + if d.SearchFunc == nil { + return nil, miaosic.ErrNotImplemented + } + //result := make([]*miaosic.Media, 0) + //err := + //fmt.Println(result) + return d.SearchFunc(keyword) +} + +func (d *DeepcolorProvider) UpdateMedia(media *miaosic.Media) error { + if d.InfoFunc == nil { + return miaosic.ErrNotImplemented + } + return d.InfoFunc(media, media) +} + +func (d DeepcolorProvider) UpdateMediaUrl(media *miaosic.Media) error { + if d.FileFunc == nil { + return miaosic.ErrNotImplemented + } + return d.FileFunc(media, media) +} + +func (d DeepcolorProvider) UpdateMediaLyric(media *miaosic.Media) error { + if d.LyricFunc == nil { + // if no lyric func, return nil + return nil + } + return d.LyricFunc(media, media) +} diff --git a/registry.go b/registry.go new file mode 100644 index 0000000..00b6bb3 --- /dev/null +++ b/registry.go @@ -0,0 +1,26 @@ +package miaosic + +import ( + "github.com/aynakeya/deepcolor" + "github.com/aynakeya/deepcolor/dphttp" +) + +var Requester dphttp.IRequester = deepcolor.NewRestyRequester() + +var _providers map[string]MediaProvider = make(map[string]MediaProvider) + +func RegisterProvider(provider MediaProvider) { + if _, ok := _providers[provider.GetName()]; ok { + panic("provider " + provider.GetName() + " already exists") + return + } + _providers[provider.GetName()] = provider +} + +func ListAvailableProviders() []string { + var names []string + for name := range _providers { + names = append(names, name) + } + return names +}