diff --git a/README.md b/README.md new file mode 100644 index 0000000..37d7276 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# Miaosic + +Media Provider Repository + + +## Todo + +- netease music +- loginable implementation \ No newline at end of file diff --git a/go.mod b/go.mod index 1bfcdf5..1898b60 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,8 @@ 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/jinzhu/copier v0.4.0 + github.com/sahilm/fuzzy v0.1.0 github.com/spf13/cast v1.5.0 github.com/stretchr/testify v1.8.4 github.com/tidwall/gjson v1.16.0 @@ -18,7 +19,9 @@ 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/go-resty/resty/v2 v2.7.0 // indirect github.com/google/uuid v1.3.0 // indirect + github.com/kylelemons/godebug v1.1.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 diff --git a/lyric.go b/lyric.go new file mode 100644 index 0000000..d8ca341 --- /dev/null +++ b/lyric.go @@ -0,0 +1,82 @@ +package miaosic + +import ( + "github.com/spf13/cast" + "regexp" + "sort" + "strings" +) + +var timeTagRegex = regexp.MustCompile("\\[[0-9]+:[0-9]+(\\.[0-9]+)?\\]") + +type LyricLine struct { + Time float64 // in seconds + Lyric string +} + +type Lyrics struct { + Lang string + Content []LyricLine +} + +func ParseLyrics(lang string, lyrics string) Lyrics { + tmp := make(map[float64]LyricLine) + times := make([]float64, 0) + for _, line := range strings.Split(lyrics, "\n") { + lrc := timeTagRegex.ReplaceAllString(line, "") + if len(lrc) > 0 && lrc[len(lrc)-1] == '\r' { + lrc = lrc[:len(lrc)-1] + } + for _, time := range timeTagRegex.FindAllString(line, -1) { + ts := strings.Split(time[1:len(time)-1], ":") + t := cast.ToFloat64(ts[0])*60 + cast.ToFloat64(ts[1]) + times = append(times, t) + tmp[t] = LyricLine{ + Time: t, + Lyric: lrc, + } + } + } + sort.Float64s(times) + lrcs := make([]LyricLine, len(times)) + for index, time := range times { + lrcs[index] = tmp[time] + } + if len(lrcs) == 0 { + lrcs = append(lrcs, LyricLine{Time: 0, Lyric: ""}) + } + lrcs = append(lrcs, LyricLine{ + Time: lrcs[len(lrcs)-1].Time + 5, + }) + lrcs = append(lrcs, LyricLine{ + Time: 99999999999, + Lyric: "", + }) + return Lyrics{Lang: lang, Content: lrcs} +} + +func (l Lyrics) FindIndex(time float64) int { + start := 0 + end := len(l.Content) - 1 + mid := (start + end) / 2 + for start < end { + if l.Content[mid].Time <= time && time < l.Content[mid+1].Time { + return mid + } + if l.Content[mid].Time > time { + end = mid + } else { + start = mid + } + mid = (start + end) / 2 + } + return -1 +} + +func (l Lyrics) Find(time float64) LyricLine { + idx := l.FindIndex(time) + if idx == -1 { + return LyricLine{Time: 0, Lyric: ""} + } + return l.Content[idx] +} diff --git a/miaosic.go b/miaosic.go index 124c762..0195323 100644 --- a/miaosic.go +++ b/miaosic.go @@ -23,7 +23,7 @@ type Media struct { Artist string Cover Picture Album string - Lyric string + Lyric []Lyrics Url string Header map[string]string Meta MediaMeta @@ -47,3 +47,11 @@ type MediaProvider interface { UpdateMediaUrl(media *Media) error UpdateMediaLyric(media *Media) error } + +type Loginable interface { + Login(username string, password string) error + QrLogin() string + QrLoginVerify() bool + RestoreSession(session string) error + SaveSession() string +} diff --git a/providers/bilivideo/bilivideo.go b/providers/bilivideo/bilivideo.go index c592c3d..91480c5 100644 --- a/providers/bilivideo/bilivideo.go +++ b/providers/bilivideo/bilivideo.go @@ -2,8 +2,10 @@ package bilivideo import ( "errors" + "fmt" "github.com/aynakeya/deepcolor" "github.com/aynakeya/deepcolor/dphttp" + "github.com/jinzhu/copier" "github.com/spf13/cast" "github.com/tidwall/gjson" "miaosic" @@ -30,6 +32,7 @@ func NewBilibiliViedo(requester dphttp.IRequester) *BilibiliVideo { "Cookie": "buvid3=9A8B3564-BDA9-407F-B45F-D5C40786CA49167618infoc;", } pvdr := &BilibiliVideo{ + requester: requester, 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]+"), @@ -37,7 +40,7 @@ func NewBilibiliViedo(requester dphttp.IRequester) *BilibiliVideo { } pvdr.InfoFunc = pvdr.buildInfoApi() //pvdr.FileFunc = buildFileApi(requester, headers) - //pvdr.LyricFunc = buildLyricApi(requester, headers) + pvdr.LyricFunc = nil //pvdr.PlaylistFunc = buildPlaylistApi(requester, headers) pvdr.SearchFunc = pvdr.buildSearchApi() return pvdr @@ -85,7 +88,7 @@ func (b *BilibiliVideo) buildInfoApi() dphttp.ApiFunc[*miaosic.Media, *miaosic.M return deepcolor.NewGetRequestWithSingleQuery( "https://api.bilibili.com/x/web-interface/view/detail?&aid=&jsonp=jsonp", "bvid", b.header, - )(params.Meta.Identifier) + )(b.getBv(params.Meta.Identifier)) }, deepcolor.ParserGJson, func(result *gjson.Result, media *miaosic.Media) error { @@ -99,22 +102,11 @@ func (b *BilibiliVideo) buildInfoApi() dphttp.ApiFunc[*miaosic.Media, *miaosic.M }) } -//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", + "https://api.bilibili.com/x/web-interface/wbi/search/type?search_type=video&page=1", "keyword", b.header), deepcolor.ParserGJson, func(resp *gjson.Result, result *[]*miaosic.Media) error { @@ -138,91 +130,51 @@ func (b *BilibiliVideo) buildSearchApi() dphttp.ApiFuncResult[string, []*miaosic }) } -//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 -//}, -//}, -//) +func (b *BilibiliVideo) cidApi(bvid string) ([]string, error) { + return deepcolor.CreateApiResultFunc( + b.requester, + deepcolor.NewGetRequestWithSingleQuery( + "https://api.bilibili.com/x/web-interface/view/detail?&aid=&jsonp=jsonp", "bvid", b.header), + deepcolor.ParserGJson, + func(resp *gjson.Result, result *[]string) error { + for _, r := range resp.Get("data.View.pages.#.cid").Array() { + *result = append(*result, r.String()) + } + if len(*result) == 0 { + return errors.New("failed to find cid data") + } + return nil + })(bvid) +} -//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) -//} +func (b *BilibiliVideo) UpdateMediaUrl(media *miaosic.Media) error { + page := b.getPage(media.Meta.Identifier) - 1 + cids, err := b.cidApi(b.getBv(media.Meta.Identifier)) + if err != nil { + return err + } + if err != nil || page >= len(cids) { + return miaosic.ErrorExternalApi + } + return deepcolor.CreateApiFunc( + b.requester, + func(params *miaosic.Media) (*dphttp.Request, error) { + return deepcolor.NewGetRequestWithQuery( + "https://api.bilibili.com/x/player/playurl?type=&otype=json&fourk=1&qn=32&avid=", + []string{"bvid", "cid"}, b.header, + )([]string{b.getBv(media.Meta.Identifier), cids[page]}) + }, + deepcolor.ParserGJson, + func(result *gjson.Result, container *miaosic.Media) error { + uri := result.Get("data.durl.0.url").String() + if uri == "" { + return miaosic.ErrorExternalApi + } + container.Url = uri + header := make(map[string]string) + _ = copier.Copy(&header, &b.header) + header["Referer"] = fmt.Sprintf("https://www.bilibili.com/video/%s", b.getBv(container.Meta.Identifier)) + container.Header = header + return nil + })(media, media) +} diff --git a/providers/bilivideo/bilivideo_test.go b/providers/bilivideo/bilivideo_test.go index 9a5963a..ba90248 100644 --- a/providers/bilivideo/bilivideo_test.go +++ b/providers/bilivideo/bilivideo_test.go @@ -1,40 +1,29 @@ package bilivideo import ( - "AynaLivePlayer/core/adapter" - "AynaLivePlayer/core/model" + "fmt" + "github.com/aynakeya/deepcolor" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "miaosic" "regexp" "strings" "testing" ) -func TestBV_GetMusicMeta(t *testing.T) { - var api adapter.MediaProvider = BilibiliVideoAPI +var api miaosic.MediaProvider = NewBilibiliViedo(deepcolor.NewRestyRequester()) - media := model.Media{ - Meta: model.Meta{ - Name: api.GetName(), - Id: "BV1434y1q71P", - }, - } - err := api.UpdateMedia(&media) - assert.Nil(t, err) +func TestBV_GetMusicMeta(t *testing.T) { + media := api.MatchMedia("BV1434y1q71P") + require.NotNil(t, media) + require.NoError(t, api.UpdateMedia(media)) 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) + media := api.MatchMedia("BV1434y1q71P") + require.NoError(t, api.UpdateMedia(media)) + require.NoError(t, api.UpdateMediaUrl(media)) assert.True(t, strings.Contains(media.Url, "bilivideo"), media.Url) } @@ -43,41 +32,23 @@ func TestBV_Regex(t *testing.T) { } 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) + media := api.MatchMedia("BV1gA411P7ir?p=3") + require.NotNil(t, media) + require.NoError(t, api.UpdateMedia(media)) + require.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) + media := api.MatchMedia("BV1gA411P7ir?p=3") + require.NoError(t, api.UpdateMedia(media)) + require.NoError(t, api.UpdateMediaUrl(media)) assert.Equal(t, "沈默沈默", media.Artist) + fmt.Println(media.Url) } 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") + result, err := api.Search("家有女友op") + require.NoError(t, err, "Search Error") + require.NotEmpty(t, result, "Search Result Empty") t.Log(result[0]) } diff --git a/providers/kuwo/kuwo.go b/providers/kuwo/kuwo.go index b30011d..1330214 100644 --- a/providers/kuwo/kuwo.go +++ b/providers/kuwo/kuwo.go @@ -198,7 +198,7 @@ func (k *Kuwo) buildLyricApi() dphttp.ApiFunc[*miaosic.Media, *miaosic.Media] { return true }) - media.Lyric = strings.Join(lrcs, "\n") + media.Lyric = []miaosic.Lyrics{miaosic.ParseLyrics("default", strings.Join(lrcs, "\n"))} return nil }) } diff --git a/providers/kuwo/kuwo_test.go b/providers/kuwo/kuwo_test.go index c883047..9c97751 100644 --- a/providers/kuwo/kuwo_test.go +++ b/providers/kuwo/kuwo_test.go @@ -52,33 +52,27 @@ func TestKuwo_GetMusic(t *testing.T) { 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) -// } -// -//} +func TestKuwo_UpdateMediaLyric(t *testing.T) { + media := miaosic.Media{ + Meta: miaosic.MediaMeta{ + Provider: api.GetName(), + Identifier: "22804772", + }, + } + err := api.UpdateMediaLyric(&media) + require.NoError(t, err) + require.NotEmpty(t, media.Lyric) +} + +func TestKuwo_GetPlaylist(t *testing.T) { + playlist := miaosic.Playlist{ + Meta: miaosic.MediaMeta{ + Provider: api.GetName(), + Identifier: "2959147566", + }, + } + err := api.UpdatePlaylist(&playlist) + require.NoError(t, err) + require.NotEmpty(t, playlist.Medias) + t.Logf("sucessfully get %d medias", len(playlist.Medias)) +} diff --git a/providers/local/local.go b/providers/local/local.go new file mode 100644 index 0000000..fe32f48 --- /dev/null +++ b/providers/local/local.go @@ -0,0 +1,135 @@ +package local + +import ( + "miaosic" + "os" + "path" +) + +type Local struct { + localDir string + playlists map[string]*miaosic.Playlist +} + +func NewLocal(localdir string) *Local { + l := &Local{localDir: localdir, playlists: make(map[string]*miaosic.Playlist, 0)} + if err := os.MkdirAll(localdir, 0755); err != nil { + return l + } + for _, n := range getPlaylistNames(localdir) { + playlist := &miaosic.Playlist{Meta: miaosic.MediaMeta{Provider: n}} + if readLocalPlaylist(localdir, playlist) != nil { + l.playlists[playlist.Title] = playlist + } + } + return l +} + +func (l *Local) GetName() string { + return "local" +} + +func (l *Local) MatchMedia(uri string) *miaosic.Media { + return nil +} + +func (l *Local) MatchPlaylist(uri string) *miaosic.Playlist { + return nil +} + +func (l *Local) Search(keyword string) ([]*miaosic.Media, error) { + allMedias := make([]*miaosic.Media, 0) + for _, p := range l.playlists { + for _, m := range p.Medias { + allMedias = append(allMedias, m) + } + } + return RankMedia(keyword, allMedias), nil +} + +func (l *Local) UpdatePlaylist(playlist *miaosic.Playlist) error { + err := readLocalPlaylist(l.localDir, playlist) + if err != nil { + return err + } + l.playlists[playlist.Meta.Identifier] = playlist + return nil +} + +func (l *Local) UpdateMedia(media *miaosic.Media) error { + mediaPath := path.Join(l.localDir, media.Meta.Identifier) + _, err := os.Stat(mediaPath) + if err != nil { + return err + } + return readMediaFile(l.localDir, media) +} + +func (l *Local) UpdateMediaUrl(media *miaosic.Media) error { + mediaPath := path.Join(l.localDir, media.Meta.Identifier) + _, err := os.Stat(mediaPath) + if err != nil { + return err + } + media.Url = mediaPath + return nil +} + +func (l *Local) UpdateMediaLyric(media *miaosic.Media) error { + return nil +} + +// +//func (l *Local) Search(keyword string) ([]*model.Media, error) { +// allMedias := make([]*model.Media, 0) +// for _, p := range l.Playlists { +// for _, m := range p.Medias { +// allMedias = append(allMedias, m) +// } +// } +// MediaSort(keyword, allMedias) +// c := util.Min(len(allMedias), 32) +// medias := make([]*model.Media, c) +// for i := 0; i < c; i++ { +// medias[i] = allMedias[i].Copy() +// } +// return medias, nil +//} +// +//func (l *Local) SearchV1(keyword string) ([]*model.Media, error) { +// result := make([]struct { +// M *model.Media +// N int +// }, 0) +// keywords := strings.Split(keyword, " ") +// for _, p := range l.Playlists { +// for _, m := range p.Medias { +// title := strings.ToLower(m.Title) +// artist := strings.ToLower(m.Artist) +// n := 0 +// for _, k := range keywords { +// kw := strings.ToLower(k) +// if strings.Contains(title, kw) || strings.Contains(artist, kw) { +// n++ +// } +// if kw == title { +// n += 3 +// } +// } +// if n > 0 { +// result = append(result, struct { +// M *model.Media +// N int +// }{M: m, N: n}) +// } +// } +// } +// sort.Slice(result, func(i, j int) bool { +// return result[i].N > result[j].N +// }) +// medias := make([]*model.Media, len(result)) +// for i, r := range result { +// medias[i] = r.M.Copy() +// } +// return medias, nil +//} diff --git a/providers/local/local_helper.go b/providers/local/local_helper.go new file mode 100644 index 0000000..2c52c48 --- /dev/null +++ b/providers/local/local_helper.go @@ -0,0 +1,129 @@ +package local + +import ( + "github.com/dhowden/tag" + "github.com/sahilm/fuzzy" + "miaosic" + "os" + "path" + "path/filepath" + "sort" + "strings" +) + +func getPlaylistNames(localdir string) []string { + names := make([]string, 0) + items, _ := os.ReadDir(localdir) + for _, item := range items { + if item.IsDir() { + names = append(names, item.Name()) + } + } + return names +} + +// readLocalPlaylist read files under a directory +// and return a _LocalPlaylist object. +// This function assume this directory exists +func readLocalPlaylist(localdir string, playlist *miaosic.Playlist) error { + p1th := playlist.Meta.Identifier + playlist.Medias = make([]*miaosic.Media, 0) + fullPath := filepath.Join(localdir, p1th) + if _, err := os.Stat(fullPath); os.IsNotExist(err) { + return err + } + items, _ := os.ReadDir(fullPath) + for _, item := range items { + // if item is a file, read file + if !item.IsDir() { + fn := item.Name() + media := miaosic.Media{ + Meta: miaosic.MediaMeta{ + Provider: "local", + Identifier: path.Join(playlist.Meta.Identifier, fn), + }, + } + if readMediaFile(localdir, &media) != nil { + continue + } + playlist.Medias = append(playlist.Medias, &media) + } + } + return nil +} + +func _getOrDefault(s string, def string) string { + if s == "" { + return def + } + return s +} + +func readMediaFile(localdir string, media *miaosic.Media) error { + p := path.Join(localdir, media.Meta.Identifier) + f, err := os.Open(p) + if err != nil { + return err + } + defer f.Close() + meta, err := tag.ReadFrom(f) + if err != nil { + return err + } + media.Title = _getOrDefault(meta.Title(), filepath.Base(p)) + media.Artist = _getOrDefault(meta.Artist(), "Unknown") + media.Album = _getOrDefault(meta.Album(), "Unknown") + media.Lyric = []miaosic.Lyrics{miaosic.ParseLyrics("default", meta.Lyrics())} + if meta.Picture() != nil { + media.Cover.Data = meta.Picture().Data + } + return nil +} + +type mediaRanking struct { + media *miaosic.Media + score int +} + +func RankMedia(keyword string, medias []*miaosic.Media) []*miaosic.Media { + patterns := strings.Split(keyword, " ") + data := make([]*mediaRanking, 0) + + for _, media := range medias { + m := media + data = append(data, &mediaRanking{ + media: m, + score: 0, + }) + } + + for _, pattern := range patterns { + pattern = strings.ToLower(pattern) + dataStr := make([]string, 0) + for _, d := range data { + dataStr = append(dataStr, strings.ToLower(d.media.Title)) + } + for _, match := range fuzzy.Find(pattern, dataStr) { + data[match.Index].score += match.Score + } + dataStr = make([]string, 0) + for _, d := range data { + dataStr = append(dataStr, strings.ToLower(d.media.Artist)) + } + for _, match := range fuzzy.Find(pattern, dataStr) { + data[match.Index].score += match.Score + } + } + + sort.Slice(data, func(i, j int) bool { + return data[i].score > data[j].score + }) + + result := make([]*miaosic.Media, 0) + for _, d := range data { + if d.score > 0 { + result = append(result, d.media) + } + } + return result +} diff --git a/providers/local/local_test.go b/providers/local/local_test.go new file mode 100644 index 0000000..faa466c --- /dev/null +++ b/providers/local/local_test.go @@ -0,0 +1,82 @@ +package local + +import ( + "fmt" + "github.com/sahilm/fuzzy" + "miaosic" + "sort" + "strings" + "testing" +) + +var testData = []miaosic.Media{ + {Title: "Shape of You", Artist: "Ed Sheeran"}, + {Title: "Lose Yourself", Artist: "Eminem"}, + {Title: "Believer", Artist: "Imagine Dragons"}, + {Title: "Counting Stars", Artist: "OneRepublic"}, + {Title: "Rolling in the Deep", Artist: "Adele"}, + {Title: "Uptown Funk", Artist: "Mark Ronson ft. Bruno Mars"}, + {Title: "Imagine", Artist: "John Lennon"}, + {Title: "I Will Always Love You", Artist: "Whitney Houston"}, + {Title: "Smells Like Teen Spirit", Artist: "Nirvana"}, + {Title: "Billie Jean", Artist: "Michael Jackson"}, + + // Chinese songs + {Title: "平凡之路", Artist: "朴树"}, + {Title: "染", Artist: "reol"}, + {Title: "怪物", Artist: "reol"}, + {Title: "怪物", Artist: "王菲"}, + {Title: "怪物", Artist: "怪物"}, + {Title: "小幸运", Artist: "田馥甄"}, + {Title: "遥远的她", Artist: "张学友"}, + {Title: "匆匆那年", Artist: "王菲"}, + {Title: "岁月神偷", Artist: "金玟岐"}, + {Title: "突然好想你", Artist: "五月天"}, + {Title: "蓝莲花", Artist: "许巍"}, + {Title: "红豆", Artist: "王菲"}, + {Title: "夜空中最亮的星", Artist: "逃跑计划"}, + {Title: "爱情转移", Artist: "陈奕迅"}, +} + +func TestLocal_SearchTest1(t *testing.T) { + pattern := "王菲" + patterns := strings.Split(pattern, " ") + data := make([]*mediaRanking, 0) + + for _, media := range testData { + m := media + data = append(data, &mediaRanking{ + media: &m, + score: 0, + }) + } + dataStr := make([]string, 0) + for _, d := range data { + dataStr = append(dataStr, strings.ToLower(d.media.Title+" "+d.media.Artist)) + } + + for _, pattern := range patterns { + for _, match := range fuzzy.Find(pattern, dataStr) { + data[match.Index].score += match.Score + } + } + + sort.Slice(data, func(i, j int) bool { + return data[i].score > data[j].score + }) + + for _, d := range data { + fmt.Println(d.score, d.media) + } +} + +func TestLocal_SearchTest2(t *testing.T) { + data := make([]*miaosic.Media, 0) + for _, media := range testData { + m := media + data = append(data, &m) + } + for _, media := range RankMedia("怪物 reol", data) { + fmt.Println(media) + } +} diff --git a/providers/providers.go b/providers/providers.go index c26c495..1191a38 100644 --- a/providers/providers.go +++ b/providers/providers.go @@ -37,14 +37,15 @@ func (d *DeepcolorProvider) UpdateMedia(media *miaosic.Media) error { return d.InfoFunc(media, media) } -func (d DeepcolorProvider) UpdateMediaUrl(media *miaosic.Media) error { +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 { +func (d *DeepcolorProvider) UpdateMediaLyric(media *miaosic.Media) error { + media.Lyric = nil if d.LyricFunc == nil { // if no lyric func, return nil return nil