mirror of
https://github.com/AynaLivePlayer/miaosic.git
synced 2025-12-13 00:08:13 +08:00
update
This commit is contained in:
9
README.md
Normal file
9
README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Miaosic
|
||||
|
||||
Media Provider Repository
|
||||
|
||||
|
||||
## Todo
|
||||
|
||||
- netease music
|
||||
- loginable implementation
|
||||
5
go.mod
5
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
|
||||
|
||||
82
lyric.go
Normal file
82
lyric.go
Normal file
@@ -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]
|
||||
}
|
||||
10
miaosic.go
10
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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
135
providers/local/local.go
Normal file
135
providers/local/local.go
Normal file
@@ -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
|
||||
//}
|
||||
129
providers/local/local_helper.go
Normal file
129
providers/local/local_helper.go
Normal file
@@ -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
|
||||
}
|
||||
82
providers/local/local_test.go
Normal file
82
providers/local/local_test.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user