├── .gitignore ├── README.md ├── cache.go ├── main.go ├── netease_service.go ├── tools ├── init-script └── logrotate-config └── xiami_service.go /.gitignore: -------------------------------------------------------------------------------- 1 | /music-api-server 2 | *.log 3 | *.swp 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # music api server 2 | 3 | 音乐服务的api通用接口,目前已支持的音乐服务: 4 | 5 | * 虾米 6 | * 专辑 7 | * 歌曲列表 8 | * 精选集 9 | * 网易云音乐 10 | * 专辑 11 | * 歌曲列表 12 | * 歌单 13 | 14 | ## 安装(debian/ubuntu) 15 | 16 | 首先检查`GOPATH`变量是否正确设置,如果未设置,参考[这里](https://www.mawenbao.com/note/golang-summary.html#3867e350ebb33a487c4ac5f7787e1c29)进行设置。 17 | 18 | # install redis-server 19 | sudo apt-get install redis-server 20 | 21 | # install redis driver for golang 22 | go get github.com/garyburd/redigo/redis 23 | 24 | # install music api server 25 | go get github.com/mawenbao/music-api-server 26 | 27 | # install init script 28 | sudo cp $GOPATH/src/github.com/mawenbao/music-api-server/tools/init-script /etc/init.d/music-api-server 29 | 30 | # set your GOPATH in init script 31 | sudo sed -i "s|/SET/YOUR/GOPATH/HERE|`echo $GOPATH`|" /etc/init.d/music-api-server 32 | 33 | sudo chmod +x /etc/init.d/music-api-server 34 | 35 | # start music api server 36 | sudo service music-api-server start 37 | 38 | # (optional) let music api server start on boot 39 | sudo update-rc.d music-api-server defaults 40 | 41 | # (optional) install logrotate config 42 | sudo cp $GOPATH/src/github.com/mawenbao/music-api-server/tools/logrotate-config /etc/logrotate.d/music-api-server 43 | 44 | ## 更新(debian/ubuntu) 45 | 46 | # update and restart music-api-server 47 | go get -u github.com/mawenbao/music-api-server 48 | sudo service music-api-server restart 49 | 50 | # flush redis cache 51 | redis-cli FLUSHDB 52 | 53 | ## API 54 | ### Demo 55 | 56 | [https://app.mawenbao.com/music-api-server/?p=xiami&t=songlist&i=20526,1772292423&c=abc123](https://app.mawenbao.com/music-api-server/?p=xiami&t=songlist&i=20526,1772292423&c=abc123) 57 | 58 | ### 请求 59 | 60 | GET http://localhost:9099/?p=xiami&t=songlist&i=20526,1772292423&c=abc123&q=high 61 | 62 | * `localhost:9099`: 默认监听地址 63 | * `p=xiami`: 音乐API提供商,目前支持: 64 | * 虾米(xiami) 65 | * 网易云音乐(netease) 66 | * `t=songlist`: 音乐类型 67 | * songlist(xiami + netease): 歌曲列表,对应的id是半角逗号分隔的多个歌曲id 68 | * album(xiami + netease): 音乐专辑,对应的id为专辑id 69 | * collect(xiami): 虾米的精选集,对应的id为精选集id 70 | * playlist(netease): 网易云音乐的歌单,对应的id为歌单(playlist)id 71 | * `i=20526,1772292423`: 歌曲/专辑/精选集/歌单的id,歌曲列表类型可用半角逗号分割多个歌曲id 72 | * `c=abc123`: 使用jsonp方式返回数据,实际返回为`abc123({songs: ...});` 73 | * `q=high`: 音乐品质,high表示优先返回高比特率的音乐链接,**该参数目前仅对网易云音乐有效**。 74 | * high: 高品音质 75 | * medium: 普通音质 76 | * low: 低品音质 77 | 78 | ### 返回 79 | 80 | { 81 | "status": "返回状态,ok为正常,failed表示出错并设置msg", 82 | "msg": "如果status为failed,这里会保存错误信息", 83 | "songs": [ 84 | { 85 | "name": "歌曲名称", 86 | "url": "歌曲播放地址", 87 | "artists": "演唱者", 88 | "provider": "音乐提供商" 89 | "lrc_url": "歌词文件地址(可能没有)", 90 | } 91 | ] 92 | } 93 | 94 | 如果有`c=abc123`请求参数,则实际以[jsonp](http://en.wikipedia.org/wiki/JSONP)方式返回数据。 95 | 96 | ## Thanks 97 | * [Wordpress Hermit Player](http://mufeng.me/hermit-for-wordpress.html) 98 | * [网易云音乐API分析](https://github.com/yanunon/NeteaseCloudMusic/wiki/%E7%BD%91%E6%98%93%E4%BA%91%E9%9F%B3%E4%B9%90API%E5%88%86%E6%9E%90) 99 | 100 | -------------------------------------------------------------------------------- /cache.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "github.com/garyburd/redigo/redis" 7 | "io/ioutil" 8 | "log" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | const ( 14 | gCacheKeyPrefix = "mas:" 15 | gUrlCacheKeyPrefix = "url:" 16 | ) 17 | 18 | var ( 19 | // minimize cache key length 20 | gUrlKeyReplacer = strings.NewReplacer( 21 | "http://", "", 22 | "api.xiami.com/web?v=2.0&app_key=1&r=", "xiami:", 23 | "/detail&", "_", 24 | "music.163.com", "163", 25 | "/api", "", 26 | ) 27 | 28 | gRedisPool = &redis.Pool{ 29 | MaxIdle: 3, 30 | IdleTimeout: 240 * time.Second, 31 | Dial: func() (redis.Conn, error) { 32 | c, err := redis.Dial("tcp", *gFlagRedisAddr) 33 | if nil != err { 34 | log.Printf("failed to connect to redis server %s: %s", *gFlagRedisAddr, err) 35 | return nil, err 36 | } 37 | return c, err 38 | }, 39 | TestOnBorrow: func(c redis.Conn, t time.Time) error { 40 | _, err := c.Do("PING") 41 | if nil != err { 42 | log.Printf("failed to ping redis server %s: %s", *gFlagRedisAddr, err) 43 | } 44 | return err 45 | }, 46 | } 47 | ) 48 | 49 | func GenUrlCacheKey(url string) string { 50 | if "" == url { 51 | log.Println("failed to generate url cache key: url is empty") 52 | return "" 53 | } 54 | return gCacheKeyPrefix + gUrlCacheKeyPrefix + gUrlKeyReplacer.Replace(url) 55 | } 56 | 57 | func GetCache(key string, decompress bool) []byte { 58 | if "" == key { 59 | return nil 60 | } 61 | 62 | // get from redis cache 63 | redisConn := gRedisPool.Get() 64 | defer redisConn.Close() 65 | value, err := redisConn.Do("GET", key) 66 | if nil != err { 67 | log.Printf("failed to get from redis server %s: %s", *gFlagRedisAddr, err) 68 | return nil 69 | } 70 | if nil == value { 71 | return nil 72 | } 73 | 74 | valueBytes := value.([]byte) 75 | if !decompress { 76 | return valueBytes 77 | } 78 | // decompress value 79 | buff := bytes.NewBuffer(valueBytes) 80 | gzipRdr, err := gzip.NewReader(buff) 81 | if nil != err { 82 | log.Printf("failed to decompress cached value: %s", err) 83 | return nil 84 | } 85 | defer gzipRdr.Close() 86 | valueBytes, err = ioutil.ReadAll(gzipRdr) 87 | if nil != err { 88 | log.Printf("failed to read cached value: %s", err) 89 | return nil 90 | } 91 | return valueBytes 92 | } 93 | 94 | func SetCache(key string, value []byte, expires time.Duration, compress bool) bool { 95 | if "" == key { 96 | return false 97 | } 98 | 99 | var err error 100 | if compress { 101 | // compress value 102 | var buff bytes.Buffer 103 | gzipWtr := gzip.NewWriter(&buff) 104 | _, err = gzipWtr.Write(value) 105 | if nil != err { 106 | log.Printf("failed to compress value: %s", err) 107 | return false 108 | } 109 | err = gzipWtr.Close() 110 | if nil != err { 111 | log.Printf("failed to compress value: %s", err) 112 | return false 113 | } 114 | value = buff.Bytes() 115 | } 116 | 117 | // save value in redis cache 118 | redisConn := gRedisPool.Get() 119 | defer redisConn.Close() 120 | if expires != 0 { 121 | _, err = redisConn.Do("SETEX", key, expires.Seconds(), value) 122 | } else { 123 | // no expiration if expires is 0 124 | _, err = redisConn.Do("SET", key, value) 125 | } 126 | if nil != err { 127 | log.Printf("failed to send value to redis server %s: %s", *gFlagRedisAddr, err) 128 | return false 129 | } 130 | return true 131 | } 132 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "fmt" 7 | "io/ioutil" 8 | "log" 9 | "net/http" 10 | "net/url" 11 | "os" 12 | "reflect" 13 | "runtime" 14 | "strconv" 15 | "strings" 16 | "time" 17 | ) 18 | 19 | const ( 20 | gMusicQualityLow = "low" 21 | gMusicQualityMedium = "medium" 22 | gMusicQualityHigh = "high" 23 | ) 24 | 25 | var ( 26 | // command line options 27 | gFlagRedisAddr = flag.String("redis", "localhost:6379", "address(host:port) of redis server") 28 | gFlagListenPort = flag.Int("port", 9099, "port to listen on") 29 | gFlagLogfile = flag.String("log", "", "path of the log file") 30 | gFlagCacheExpiration = flag.Int("expire", 3600, "expiry time(in seconds) of redis cache, default is one hour, 0 means no expiration") 31 | gCacheExpiryTime = time.Hour 32 | // available music service functions 33 | gAvailableGetMusicFuncs = []GetMusicFunc{ 34 | GetXiamiSongList, 35 | GetXiamiAlbum, 36 | GetXiamiCollect, 37 | GetNeteaseSongList, 38 | GetNeteaseAlbum, 39 | GetNeteasePlayList, 40 | } 41 | gGetMusicFuncMap = make(map[string]GetMusicFunc) 42 | ) 43 | 44 | func init() { 45 | // init getmusic function map 46 | for _, f := range gAvailableGetMusicFuncs { 47 | gGetMusicFuncMap[getLowerFuncName(f)] = f 48 | } 49 | } 50 | 51 | type GetMusicFunc func(*ReqParams) *SongList 52 | 53 | type ReqParams struct { 54 | ID, 55 | Callback, 56 | Provider, 57 | Quality, 58 | ReqType string 59 | } 60 | 61 | func ParseReqParams(queries url.Values) *ReqParams { 62 | params := &ReqParams{} 63 | params.Callback = strings.ToLower(queries.Get("c")) 64 | params.Provider = strings.ToLower(queries.Get("p")) 65 | params.ReqType = strings.ToLower(strings.TrimSpace(queries.Get("t"))) 66 | params.ID = strings.TrimSpace(queries.Get("i")) 67 | params.Quality = strings.ToLower(queries.Get("q")) 68 | return params 69 | } 70 | 71 | type Song struct { 72 | Name string `json:"name"` 73 | Url string `json:"url"` 74 | LrcUrl string `json:"lrc_url"` 75 | Artists string `json:"artists"` 76 | Provider string `json:"provider"` 77 | } 78 | 79 | func (s *Song) ToJsonString() string { 80 | jsonStr, err := json.Marshal(s) 81 | if err != nil { 82 | log.Printf("error generating song json string: %s", err) 83 | return "error" 84 | } 85 | return string(jsonStr) 86 | } 87 | 88 | type SongList struct { 89 | Status string `json:"status"` 90 | ErrMsg string `json:"msg"` 91 | Songs []Song `json:"songs"` 92 | } 93 | 94 | func NewSongList() *SongList { 95 | return &SongList{ 96 | Status: "ok", 97 | ErrMsg: "", 98 | Songs: []Song{}, 99 | } 100 | } 101 | 102 | func (sl *SongList) SetAndLogErrorf(format string, args ...interface{}) *SongList { 103 | sl.Status = "failed" 104 | sl.ErrMsg = fmt.Sprintf(format, args...) 105 | log.Printf(sl.ErrMsg) 106 | return sl 107 | } 108 | 109 | func (sl *SongList) IsFailed() bool { 110 | if "failed" == sl.Status || "" != sl.ErrMsg { 111 | return true 112 | } 113 | return false 114 | } 115 | 116 | func (sl *SongList) AddSong(s *Song) *SongList { 117 | sl.Songs = append(sl.Songs, *s) 118 | return sl 119 | } 120 | 121 | func (sl *SongList) Concat(ol *SongList) *SongList { 122 | if ol == nil || ol.Songs == nil { 123 | return sl 124 | } 125 | if ol.IsFailed() || nil == sl.Songs { 126 | return ol 127 | } 128 | sl.Songs = append(sl.Songs, ol.Songs...) 129 | return sl 130 | } 131 | 132 | func (sl *SongList) ToJsonString() string { 133 | jsonStr, err := json.Marshal(sl) 134 | if err != nil { 135 | log.Printf("error generating json string: %s", err) 136 | return "error" 137 | } 138 | return string(jsonStr) 139 | } 140 | 141 | func GetUrl(client *http.Client, url string) []byte { 142 | cacheKey := GenUrlCacheKey(url) 143 | if "" == cacheKey { 144 | return nil 145 | } 146 | // try to load from cache first 147 | body := GetCache(cacheKey, true) 148 | if nil != body { 149 | return body 150 | } 151 | 152 | // cache missed, do http request 153 | resp, err := client.Get(url) 154 | if err != nil { 155 | log.Printf("error get url %s: %s", url, err) 156 | return nil 157 | } 158 | defer resp.Body.Close() 159 | body, err = ioutil.ReadAll(resp.Body) 160 | if err != nil { 161 | log.Printf("error getting response body from url %s: %s", url, err) 162 | return nil 163 | } 164 | // update cache, with compression 165 | SetCache(cacheKey, body, gCacheExpiryTime, true) 166 | return body 167 | } 168 | 169 | func showUsage() { 170 | fmt.Println("Usage %s [-redis ][-port ][-log ][-expire ]") 171 | fmt.Println("Flags:") 172 | flag.PrintDefaults() 173 | } 174 | 175 | func getLowerFuncName(i interface{}) string { 176 | funcName := strings.ToLower(runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()) 177 | return strings.Split(funcName, ".")[1] 178 | } 179 | 180 | func createServMux() http.Handler { 181 | servMux := http.NewServeMux() 182 | servMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 183 | params := ParseReqParams(r.URL.Query()) 184 | myGetMusicFunc, ok := gGetMusicFuncMap["get"+params.Provider+params.ReqType] 185 | var result []byte 186 | if !ok { 187 | result = []byte(NewSongList().SetAndLogErrorf("invalid request arguments").ToJsonString()) 188 | } else { 189 | // fetch and parse music data 190 | result = []byte(myGetMusicFunc(params).ToJsonString()) 191 | } 192 | if "" != params.Callback { 193 | // jsonp 194 | result = []byte(params.Callback + "(" + string(result) + ");") 195 | } 196 | w.Write(result) 197 | }) 198 | return servMux 199 | } 200 | 201 | func main() { 202 | flag.Usage = showUsage 203 | flag.Parse() 204 | 205 | // init log 206 | if "" != *gFlagLogfile { 207 | logfile, err := os.OpenFile(*gFlagLogfile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) 208 | if nil != err { 209 | log.Fatalf("failed to open/create log file %s: %s", *gFlagLogfile, err) 210 | } 211 | defer logfile.Close() 212 | log.SetOutput(logfile) 213 | } 214 | 215 | // set cache expiry time 216 | gCacheExpiryTime = time.Duration(*gFlagCacheExpiration) * time.Second 217 | 218 | // init http server 219 | serverAddr := ":" + strconv.Itoa(*gFlagListenPort) 220 | httpServer := &http.Server{ 221 | Addr: serverAddr, 222 | Handler: createServMux(), 223 | } 224 | // start server 225 | log.Println("Start music api server ...") 226 | log.Printf("Listening at %s ...", serverAddr) 227 | log.Fatal(httpServer.ListenAndServe()) 228 | } 229 | -------------------------------------------------------------------------------- /netease_service.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "crypto/md5" 6 | "encoding/base64" 7 | "encoding/json" 8 | "fmt" 9 | "log" 10 | "net/http" 11 | "net/http/cookiejar" 12 | "net/url" 13 | "strconv" 14 | "strings" 15 | ) 16 | 17 | const ( 18 | gNeteaseRetOk = 200 19 | gNeteaseProvider = "http://music.163.com/" 20 | gNeteaseAPIUrlBase = "http://music.163.com/api" 21 | gNeteaseAlbumUrl = "/album/" 22 | gNeteaseSongListUrl = "/song/detail?ids=[%s]" 23 | gNeteasePlayListUrl = "/playlist/detail?id=" 24 | gNeteaseEIDCacheKeyPrefix = "163eid:" // encrypted dfsId 25 | gNeteaseMusicCDNUrlF = "http://p1.music.126.net/%s/%s.mp3" 26 | ) 27 | 28 | var ( 29 | gNeteaseClient = &http.Client{} 30 | gNeteaseEIDReplacer = strings.NewReplacer( 31 | "/", "_", 32 | "+", "-", 33 | ) 34 | // bypass cross-domain problem 35 | gNeteaseUrlReplacer = strings.NewReplacer( 36 | "http://m", "http://p", 37 | ) 38 | ) 39 | 40 | func init() { 41 | // init netease http client 42 | cookies, err := cookiejar.New(nil) 43 | if nil != err { 44 | log.Fatal("failed to init netease httpclient cookiejar: %s", err) 45 | } 46 | apiUrl, err := url.Parse(gNeteaseAPIUrlBase) 47 | if nil != err { 48 | log.Fatal("failed to parse netease api url %s: %s", gNeteaseAPIUrlBase, err) 49 | } 50 | // netease api requires some cookies to work 51 | cookies.SetCookies(apiUrl, []*http.Cookie{ 52 | &http.Cookie{Name: "appver", Value: "1.4.1.62460"}, 53 | &http.Cookie{Name: "os", Value: "pc"}, 54 | &http.Cookie{Name: "osver", Value: "Microsoft-Windows-7-Ultimate-Edition-build-7600-64bit"}, 55 | }) 56 | gNeteaseClient.Jar = cookies 57 | } 58 | 59 | type NeteaseRetStatus struct { 60 | StatusCode int `json:"code"` 61 | Message string `json:"message"` 62 | } 63 | 64 | type NeteaseAlbumRet struct { 65 | NeteaseRetStatus 66 | Album NeteaseAlbum `json:"album"` 67 | } 68 | 69 | type NeteaseAlbum struct { 70 | Songs []NeteaseSong `json:"songs"` 71 | } 72 | 73 | type NeteaseSongListRet struct { 74 | NeteaseRetStatus 75 | Songs []NeteaseSong `json:"songs"` 76 | } 77 | 78 | type NeteasePlayListRet struct { 79 | NeteaseRetStatus 80 | Result NeteasePlayList `json:"result"` 81 | } 82 | 83 | type NeteasePlayList struct { 84 | Songs []NeteaseSong `json:"tracks"` 85 | } 86 | 87 | type NeteaseSong struct { 88 | Artists []NeteaseArtist `json:"artists"` 89 | Name string `json:"name"` 90 | Url string `json:"mp3Url"` 91 | HighQualityMusic NeteaseMusicDetail `json:"hMusic"` 92 | MediumQualityMusic NeteaseMusicDetail `json:"mMusic"` 93 | LowQualityMusic NeteaseMusicDetail `json:"lMusic"` 94 | } 95 | 96 | func (song *NeteaseSong) UpdateUrl(quality string) *NeteaseSong { 97 | if "" == quality || gMusicQualityMedium == quality { 98 | song.Url = gNeteaseUrlReplacer.Replace(song.Url) 99 | return song 100 | } 101 | musicDetail := &song.HighQualityMusic 102 | if gMusicQualityLow == quality { 103 | musicDetail = &song.LowQualityMusic 104 | } 105 | song.Url = musicDetail.MakeUrl() 106 | return song 107 | } 108 | 109 | type NeteaseMusicDetail struct { 110 | Bitrate int `json:"bitrate"` 111 | DfsID int `json:"dfsId"` 112 | } 113 | 114 | func (md *NeteaseMusicDetail) MakeUrl() string { 115 | strDfsID := strconv.Itoa(md.DfsID) 116 | // load eid from cache first 117 | eidKey := gCacheKeyPrefix + gNeteaseEIDCacheKeyPrefix + strDfsID 118 | eid := GetCache(eidKey, false) 119 | if nil == eid { 120 | // build encrypted dfsId, see https://github.com/yanunon/NeteaseCloudMusic/wiki/网易云音乐API分析#歌曲id加密代码 121 | byte1 := []byte("3go8&$8*3*3h0k(2)2") 122 | byte2 := []byte(strDfsID) 123 | byte1Len := len(byte1) 124 | for i := range byte2 { 125 | byte2[i] = byte2[i] ^ byte1[i%byte1Len] 126 | } 127 | sum := md5.Sum(byte2) 128 | var buff bytes.Buffer 129 | enc := base64.NewEncoder(base64.StdEncoding, &buff) 130 | _, err := enc.Write(sum[:]) 131 | if nil != err { 132 | log.Printf("error encoding(base64) netease dfsId %s:%s", strDfsID, err) 133 | return "" 134 | } 135 | enc.Close() 136 | eid = []byte(gNeteaseEIDReplacer.Replace(buff.String())) 137 | } 138 | // update cache, no expiration, no compression 139 | SetCache(eidKey, eid, 0, false) 140 | return fmt.Sprintf(gNeteaseMusicCDNUrlF, eid, strDfsID) 141 | } 142 | 143 | func (ns *NeteaseSong) ArtistsString() string { 144 | arts := "" 145 | for i, _ := range ns.Artists { 146 | arts += (ns.Artists[i].Name + ",") 147 | } 148 | return strings.TrimRight(arts, ",") 149 | } 150 | 151 | type NeteaseArtist struct { 152 | Name string `json:"name"` 153 | } 154 | 155 | func GetNeteaseAlbum(params *ReqParams) *SongList { 156 | url := gNeteaseAPIUrlBase + gNeteaseAlbumUrl + params.ID 157 | ret := GetUrl(gNeteaseClient, url) 158 | sl := NewSongList() 159 | if nil == ret { 160 | return sl.SetAndLogErrorf("error accessing url %s", url) 161 | } 162 | 163 | var albumRet NeteaseAlbumRet 164 | err := json.Unmarshal(ret, &albumRet) 165 | if nil != err { 166 | return sl.SetAndLogErrorf("error parsing album return data from url %s: %s", url, err) 167 | } 168 | if gNeteaseRetOk != albumRet.StatusCode { 169 | return sl.SetAndLogErrorf("error getting url %s: %s", url, albumRet.Message) 170 | } 171 | 172 | for i, _ := range albumRet.Album.Songs { 173 | song := (&albumRet.Album.Songs[i]).UpdateUrl(params.Quality) 174 | sl.AddSong(&Song{ 175 | Name: song.Name, 176 | Url: song.Url, 177 | Artists: song.ArtistsString(), 178 | Provider: gNeteaseProvider, 179 | }) 180 | } 181 | return sl 182 | } 183 | 184 | func GetNeteaseSongList(params *ReqParams) *SongList { 185 | url := fmt.Sprintf(gNeteaseAPIUrlBase+gNeteaseSongListUrl, params.ID) 186 | ret := GetUrl(gNeteaseClient, url) 187 | sl := NewSongList() 188 | if nil == ret { 189 | return sl.SetAndLogErrorf("error accessing url %s", url) 190 | } 191 | 192 | var songlistRet NeteaseSongListRet 193 | err := json.Unmarshal(ret, &songlistRet) 194 | if nil != err { 195 | return sl.SetAndLogErrorf("error parsing songlist return data from url %s: %s", url, err) 196 | } 197 | if gNeteaseRetOk != songlistRet.StatusCode { 198 | return sl.SetAndLogErrorf("error getting url %s: %s", url, songlistRet.Message) 199 | } 200 | 201 | for i, _ := range songlistRet.Songs { 202 | song := (&songlistRet.Songs[i]).UpdateUrl(params.Quality) 203 | sl.AddSong(&Song{ 204 | Name: song.Name, 205 | Url: song.Url, 206 | Artists: song.ArtistsString(), 207 | Provider: gNeteaseProvider, 208 | }) 209 | } 210 | return sl 211 | } 212 | 213 | func GetNeteasePlayList(params *ReqParams) *SongList { 214 | url := gNeteaseAPIUrlBase + gNeteasePlayListUrl + params.ID 215 | ret := GetUrl(gNeteaseClient, url) 216 | sl := NewSongList() 217 | if nil == ret { 218 | return sl.SetAndLogErrorf("error accessing url %s", url) 219 | } 220 | 221 | var playlistRet NeteasePlayListRet 222 | err := json.Unmarshal(ret, &playlistRet) 223 | if nil != err { 224 | return sl.SetAndLogErrorf("error parsing playlist return data from url %s: %s", url, err) 225 | } 226 | if gNeteaseRetOk != playlistRet.StatusCode { 227 | return sl.SetAndLogErrorf("error getting url %s: %s", url, playlistRet.Message) 228 | } 229 | 230 | for i, _ := range playlistRet.Result.Songs { 231 | song := (&playlistRet.Result.Songs[i]).UpdateUrl(params.Quality) 232 | sl.AddSong(&Song{ 233 | Name: song.Name, 234 | Url: song.Url, 235 | Artists: song.ArtistsString(), 236 | Provider: gNeteaseProvider, 237 | }) 238 | } 239 | return sl 240 | 241 | } 242 | -------------------------------------------------------------------------------- /tools/init-script: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | GOPATH=/SET/YOUR/GOPATH/HERE 4 | 5 | DAEMON="music-api-server" 6 | DAEMON_PATH=${GOPATH}/bin/${DAEMON} 7 | NAME="music api server" 8 | PID_FILE=/var/run/${DAEMON}.pid 9 | 10 | # music-api-server cmdline options 11 | LOG_FILE=/var/log/${DAEMON}.log 12 | LISTEN_PORT=9099 13 | REDIS_ADDR=localhost:6379 14 | EXPIRY_TIME=7200 # 2 hours 15 | 16 | set -e 17 | 18 | start() { 19 | echo "Starting ${NAME}..." 20 | if [[ -f ${PID_FILE} ]]; then 21 | echo "Failed to start ${NAME}: pid file ${PID_FILE} already exists." 22 | exit 1 23 | fi 24 | start-stop-daemon -p ${PID_FILE} -m --background -S -x ${DAEMON_PATH} -- \ 25 | -log ${LOG_FILE} -expire ${EXPIRY_TIME} -port ${LISTEN_PORT} -redis ${REDIS_ADDR} 26 | sleep 1 27 | if [[ -f ${PID_FILE} && -f ${LOG_FILE} ]]; then 28 | echo "Started ${NAME} successfully" 29 | else 30 | echo "Failed to start ${NAME}" 31 | fi 32 | } 33 | 34 | stop() { 35 | echo "Stopping ${NAME}..." 36 | if [[ ! -f ${PID_FILE} ]]; then 37 | echo "Failed to stop ${NAME}: pid file ${PID_FILE} not exists." 38 | exit 1 39 | fi 40 | start-stop-daemon -K -p ${PID_FILE} 41 | rm ${PID_FILE} 42 | } 43 | 44 | status () { 45 | if [[ ! -f ${PID_FILE} ]]; then 46 | echo "${NAME} is not running" 47 | else 48 | ps aux | grep -v grep | grep `cat ${PID_FILE}` 49 | fi 50 | } 51 | 52 | case "${1}" in 53 | start) 54 | start 55 | ;; 56 | stop) 57 | stop 58 | ;; 59 | restart) 60 | stop 61 | start 62 | ;; 63 | status) 64 | status 65 | ;; 66 | *) 67 | echo "Usage: service ${0} {start|stop|restart|status}" 68 | exit 1 69 | ;; 70 | esac 71 | 72 | -------------------------------------------------------------------------------- /tools/logrotate-config: -------------------------------------------------------------------------------- 1 | /var/log/music-api-server.log { 2 | rotate 10 3 | size 2M 4 | compress 5 | missingok 6 | } 7 | 8 | -------------------------------------------------------------------------------- /xiami_service.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "log" 7 | "net/http" 8 | "strings" 9 | ) 10 | 11 | const ( 12 | gXiamiSongSplitter = "," 13 | gXiamiRetOK = 0 14 | gXiamiRetFail = "failed" 15 | gXiamiTokenName = "_xiamitoken" 16 | gXiamiProvider = "http://www.xiami.com/" 17 | gXiamiAPIUrlBase = "http://api.xiami.com/web?v=2.0&app_key=1&r=" 18 | gXiamiSongUrl = "song/detail&id=" 19 | gXiamiAlbumUrl = "album/detail&id=" 20 | gXiamiCollectUrl = "collect/detail&type=collectId&id=" 21 | ) 22 | 23 | var ( 24 | gXiamiTokenVal = "_xiamitoken=" 25 | gXiamiClient = &http.Client{} 26 | gXiamiHttpHeaders = map[string]string{ 27 | "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 7_1_2 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D257 Safari/9537.53", 28 | "Referer": "http://m.xiami.com/", 29 | "Host": "m.xiami.com", 30 | "Proxy-Connection": "keep-alive", 31 | "X-Requested-With": "XMLHttpRequest", 32 | "X-FORWARDED-FOR": "42.156.140.238", 33 | "CLIENT-IP": "42.156.140.238", 34 | } 35 | ) 36 | 37 | type XiamiRetStatus struct { 38 | Status int `json:"state"` 39 | Message string `json:"message"` 40 | } 41 | 42 | type XiamiSong struct { 43 | Name string `json:"song_name"` 44 | Url string `json:"listen_file"` 45 | Artist string `json:"singers"` 46 | } 47 | 48 | type XiamiRetData struct { 49 | Songs []XiamiSong `json:"songs"` 50 | Song XiamiSong `json:"song"` 51 | } 52 | 53 | type XiamiRet struct { 54 | XiamiRetStatus 55 | XiamiRetData `json:"data"` 56 | } 57 | 58 | func init() { 59 | if !setXiamiToken() { 60 | log.Fatalln("failed to set xiami token") 61 | } 62 | } 63 | 64 | func isXiamiTokenSet() bool { 65 | return (gXiamiTokenName + "=") != gXiamiTokenVal 66 | } 67 | 68 | func setXiamiToken() bool { 69 | if isXiamiTokenSet() { 70 | return true 71 | } 72 | tokenUrl := "http://m.xiami.com" 73 | req, err := http.NewRequest("HEAD", tokenUrl, nil) 74 | if nil != err { 75 | log.Printf("failed to create http request for url %s: %s", tokenUrl, err) 76 | return false 77 | } 78 | for k, v := range gXiamiHttpHeaders { 79 | req.Header.Add(k, v) 80 | } 81 | resp, err := gXiamiClient.Do(req) 82 | defer resp.Body.Close() 83 | if nil != err { 84 | log.Printf("error get url %s: %s", tokenUrl, err) 85 | return false 86 | } 87 | 88 | // parse xiami token from cookies 89 | for _, c := range resp.Cookies() { 90 | if gXiamiTokenName == c.Name { 91 | gXiamiTokenVal += c.Value 92 | } 93 | } 94 | if !isXiamiTokenSet() { 95 | log.Printf("error get xiami token from response cookies") 96 | return false 97 | } 98 | return true 99 | } 100 | 101 | func getXiamiUrl(client *http.Client, url string) []byte { 102 | cacheKey := GenUrlCacheKey(url) 103 | if "" == cacheKey { 104 | return nil 105 | } 106 | // try to load from cache first 107 | body := GetCache(cacheKey, true) 108 | if nil != body { 109 | return body 110 | } 111 | 112 | // do the real request 113 | urlWithToken := url + "&" + gXiamiTokenVal 114 | req, err := http.NewRequest("GET", urlWithToken, nil) 115 | if nil != err { 116 | log.Printf("failed to create http request for url %s: %s", urlWithToken, err) 117 | return nil 118 | } 119 | for k, v := range gXiamiHttpHeaders { 120 | req.Header.Add(k, v) 121 | } 122 | req.Header.Add("Cookie", gXiamiTokenVal) 123 | resp, err := gXiamiClient.Do(req) 124 | if nil != err { 125 | log.Printf("error get url %s: %s") 126 | return nil 127 | } 128 | defer resp.Body.Close() 129 | body, err = ioutil.ReadAll(resp.Body) 130 | if err != nil { 131 | log.Printf("error getting response body from url %s: %s", url, err) 132 | return nil 133 | } 134 | 135 | // update cache, with compression 136 | SetCache(cacheKey, body, gCacheExpiryTime, true) 137 | return body 138 | } 139 | 140 | func parseXiamiRetData(url string, data []byte) *SongList { 141 | sl := NewSongList() 142 | if nil == data { 143 | return sl.SetAndLogErrorf("error accessing url %s", url) 144 | } 145 | 146 | var songret XiamiRet 147 | err := json.Unmarshal(data, &songret) 148 | if nil != err { 149 | return sl.SetAndLogErrorf("error parsing xiami return data from url %s: %s", url, err) 150 | } 151 | 152 | if gXiamiRetOK != songret.Status { 153 | return sl.SetAndLogErrorf("error getting url %s: %s", url, songret.Message) 154 | } 155 | 156 | if 0 != len(songret.Songs) { 157 | // for album and collect 158 | for i, _ := range songret.Songs { 159 | song := &songret.Songs[i] 160 | sl.AddSong(&Song{ 161 | Name: song.Name, 162 | Url: song.Url, 163 | Artists: song.Artist, 164 | Provider: gXiamiProvider, 165 | }) 166 | } 167 | return sl 168 | } else if "" != songret.Song.Url { 169 | sl.AddSong(&Song{ 170 | Name: songret.Song.Name, 171 | Url: songret.Song.Url, 172 | Artists: songret.Song.Artist, 173 | Provider: gXiamiProvider, 174 | }) 175 | return sl 176 | } else { 177 | return sl.SetAndLogErrorf("invalid xiami url: %s", url) 178 | } 179 | } 180 | 181 | func getXiamiSong(songID string) *SongList { 182 | url := gXiamiAPIUrlBase + gXiamiSongUrl + strings.TrimSpace(songID) 183 | ret := getXiamiUrl(gXiamiClient, url) 184 | return parseXiamiRetData(url, ret) 185 | } 186 | 187 | func GetXiamiSongList(params *ReqParams) *SongList { 188 | sl := NewSongList() 189 | for _, sid := range strings.Split(params.ID, gXiamiSongSplitter) { 190 | singleSL := getXiamiSong(strings.TrimSpace(sid)) 191 | if singleSL.IsFailed() { 192 | return singleSL 193 | } 194 | sl.Concat(singleSL) 195 | } 196 | return sl 197 | } 198 | 199 | func GetXiamiCollect(params *ReqParams) *SongList { 200 | url := gXiamiAPIUrlBase + gXiamiCollectUrl + strings.TrimSpace(params.ID) 201 | ret := getXiamiUrl(gXiamiClient, url) 202 | return parseXiamiRetData(url, ret) 203 | } 204 | 205 | func GetXiamiAlbum(params *ReqParams) *SongList { 206 | url := gXiamiAPIUrlBase + gXiamiAlbumUrl + strings.TrimSpace(params.ID) 207 | ret := getXiamiUrl(gXiamiClient, url) 208 | return parseXiamiRetData(url, ret) 209 | } 210 | --------------------------------------------------------------------------------