diff --git a/app.go b/app.go index fba94b9..73a6070 100644 --- a/app.go +++ b/app.go @@ -42,6 +42,9 @@ func main() { log.Println("searchLimit=", *config.SearchLimit) log.Println("blockUpdate=", *config.BlockUpdate) log.Println("BlockAds=", *config.BlockAds) + log.Println("EnableLocalVip=", *config.EnableLocalVip) + log.Println("UnlockSoundEffects=", *config.UnlockSoundEffects) + log.Println("QQCookieFile=", *config.QQCookieFile) if host.InitHosts() == nil { //go func() { // // // terminal: $ go tool pprof -http=:8081 http://localhost:6060/debug/pprof/heap diff --git a/common/common.go b/common/common.go index 3871ff8..deb33c3 100644 --- a/common/common.go +++ b/common/common.go @@ -47,6 +47,7 @@ const ( KuWoTag PlatformIdTag = "90000" MiGuTag PlatformIdTag = "90001" KuGouTag PlatformIdTag = "90002" + QQTag PlatformIdTag = "90003" ) type SearchOrderBy int32 diff --git a/config/config.go b/config/config.go index 54246b3..6c07873 100644 --- a/config/config.go +++ b/config/config.go @@ -15,19 +15,22 @@ import ( ) var ( - Port = flag.Int("p", 80, "specify server port,such as : \"80\"") - TLSPort = flag.Int("sp", 443, "specify server tls port,such as : \"443\"") - Source = flag.String("o", "kuwo", "specify server source,such as : \"kuwo\"") - CertFile = flag.String("c", "./server.crt", "specify server cert,such as : \"server.crt\"") - KeyFile = flag.String("k", "./server.key", "specify server cert key ,such as : \"server.key\"") - LogFile = flag.String("l", "", "specify log file ,such as : \"/var/log/unblockNeteaseMusic.log\"") - Mode = flag.Int("m", 1, "specify running mode(1:hosts) ,such as : \"1\"") - V = flag.Bool("v", false, "display version info") - EndPoint = flag.Bool("e", false, "enable replace song url") - ForceBestQuality = flag.Bool("b", false, "force the best music quality") - SearchLimit = flag.Int("sl", 0, "specify the number of songs searched on other platforms(the range is 0 to 3) ,such as : \"1\"") - BlockUpdate = flag.Bool("bu", false, "block version update message") - BlockAds = flag.Bool("ba", false, "block advertising requests") + Port = flag.Int("p", 80, "specify server port,such as : \"80\"") + TLSPort = flag.Int("sp", 443, "specify server tls port,such as : \"443\"") + Source = flag.String("o", "kuwo", "specify server source,such as : \"kuwo\"") + CertFile = flag.String("c", "./server.crt", "specify server cert,such as : \"server.crt\"") + KeyFile = flag.String("k", "./server.key", "specify server cert key ,such as : \"server.key\"") + LogFile = flag.String("l", "", "specify log file ,such as : \"/var/log/unblockNeteaseMusic.log\"") + Mode = flag.Int("m", 1, "specify running mode(1:hosts) ,such as : \"1\"") + V = flag.Bool("v", false, "display version info") + EndPoint = flag.Bool("e", false, "enable replace song url") + ForceBestQuality = flag.Bool("b", false, "force the best music quality") + SearchLimit = flag.Int("sl", 0, "specify the number of songs searched on other platforms(the range is 0 to 3) ,such as : \"1\"") + BlockUpdate = flag.Bool("bu", false, "block version update message") + BlockAds = flag.Bool("ba", false, "block advertising requests") + EnableLocalVip = flag.Bool("lv", false, "enable local vip") + UnlockSoundEffects = flag.Bool("sef", false, "unlock SoundEffects") + QQCookieFile = flag.String("qc", "./qq.cookie", "specify cookies file ,such as : \"qq.cookie\"") ) func ValidParams() bool { @@ -62,7 +65,7 @@ func ValidParams() bool { log.Println(err) currentPath = "" } - //log.Println(currentPath) + // log.Println(currentPath) certFile, _ := filepath.Abs(*CertFile) keyFile, _ := filepath.Abs(*KeyFile) _, err = os.Open(certFile) @@ -80,8 +83,8 @@ func ValidParams() bool { logFilePath, _ := filepath.Abs(*LogFile) logFile, logErr := os.OpenFile(logFilePath, os.O_CREATE|os.O_RDWR|os.O_SYNC|os.O_APPEND, 0666) if logErr != nil { - //log.Println("Fail to find unblockNeteaseMusic.log start Failed") - //panic(logErr) + // log.Println("Fail to find unblockNeteaseMusic.log start Failed") + // panic(logErr) logFilePath, _ = filepath.Abs(currentPath + *LogFile) } else { logFile.Close() @@ -98,7 +101,7 @@ func ValidParams() bool { if err != nil { panic(err) } - if (fileInfo.Size() >> 20) > 2 { //2M + if (fileInfo.Size() >> 20) > 2 { // 2M logFile.Seek(0, io.SeekStart) logFile.Truncate(0) } diff --git a/cookiestxt/cookiestxt.go b/cookiestxt/cookiestxt.go new file mode 100644 index 0000000..b194615 --- /dev/null +++ b/cookiestxt/cookiestxt.go @@ -0,0 +1,114 @@ +// Copyright 2017 Meng Zhuo. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +// Package cookiestxt implement parser of cookies txt format that commonly supported by +// curl / wget / aria2c / chrome / firefox +// +// see http://www.cookiecentral.com/faq/#3.5 for more detail +package cookiestxt + +import ( + "bufio" + "fmt" + "io" + "net/http" + "strconv" + "strings" + "time" +) + +const ( + // http://www.cookiecentral.com/faq/#3.5 + // The domain that created AND that can read the variable. + domainIdx = iota + // A TRUE/FALSE value indicating if all machines within a given domain can access the variable. This value is set automatically by the browser, depending on the value you set for domain. + flagIdx + // The path within the domain that the variable is valid for. + pathIdx + // A TRUE/FALSE value indicating if a secure connection with the domain is needed to access the variable. + secureIdx + // The UNIX time that the variable will expire on. UNIX time is defined as the number of seconds since Jan 1, 1970 00:00:00 GMT. + expirationIdx + // The name of the variable. + nameIdx + // The value of the variable. + valueIdx +) + +const ( + httpOnlyPrefix = "#HttpOnly_" + fieldsCount = 6 +) + +// Parse cookie txt file format from input stream +func Parse(rd io.Reader) (cl []*http.Cookie, err error) { + scanner := bufio.NewScanner(rd) + var line int + for scanner.Scan() { + line++ + + trimed := strings.TrimSpace(scanner.Text()) + if len(trimed) < fieldsCount { + continue + } + + if trimed[0] == '#' && !strings.HasPrefix(trimed, httpOnlyPrefix) { + // comment + continue + } + + var c *http.Cookie + c, err = ParseLine(scanner.Text()) + if err != nil { + fmt.Errorf("cookiestxt line:%d, err:%s", line, err) + continue + } + cl = append(cl, c) + line++ + } + + err = scanner.Err() + return +} + +// ParseLine parse single cookie from one line +func ParseLine(raw string) (c *http.Cookie, err error) { + f := strings.Fields(raw) + fl := len(f) + if fl < fieldsCount { + err = fmt.Errorf("expecting fields=6, got=%d", fl) + return + } + value := "" + if fl > 6 { + value = f[valueIdx] + } + c = &http.Cookie{ + Raw: raw, + Name: f[nameIdx], + Value: value, + Path: f[pathIdx], + MaxAge: 0, + Secure: parseBool(f[secureIdx]), + } + + var ts int64 + ts, err = strconv.ParseInt(f[expirationIdx], 10, 64) + if err != nil { + return + } + c.Expires = time.Unix(ts, 0) + + c.Domain = f[domainIdx] + if strings.HasPrefix(c.Domain, httpOnlyPrefix) { + c.HttpOnly = true + c.Domain = c.Domain[len(httpOnlyPrefix):] + } + + return +} + +func parseBool(input string) bool { + return input == "TRUE" +} diff --git a/go.mod b/go.mod index 98348c9..f443823 100644 --- a/go.mod +++ b/go.mod @@ -3,5 +3,6 @@ module github.com/cnsilvan/UnblockNeteaseMusic go 1.16 require ( + github.com/mitchellh/mapstructure v1.4.2 golang.org/x/text v0.3.3 ) diff --git a/go.sum b/go.sum index fd5b10f..96f7683 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/mitchellh/mapstructure v1.4.2 h1:6h7AQ0yhTcIsmFmnAwQls75jp2Gzs4iB8W7pjMO+rqo= +github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/processor/processor.go b/processor/processor.go index f4b4a69..8d09dbf 100644 --- a/processor/processor.go +++ b/processor/processor.go @@ -6,15 +6,6 @@ import ( "crypto/md5" "encoding/hex" "encoding/json" - "io" - "io/ioutil" - "log" - "net/http" - "net/url" - "regexp" - "strconv" - "strings" - "github.com/cnsilvan/UnblockNeteaseMusic/cache" "github.com/cnsilvan/UnblockNeteaseMusic/common" "github.com/cnsilvan/UnblockNeteaseMusic/config" @@ -23,45 +14,58 @@ import ( "github.com/cnsilvan/UnblockNeteaseMusic/provider" "github.com/cnsilvan/UnblockNeteaseMusic/utils" "golang.org/x/text/width" + "io" + "io/ioutil" + "log" + "net/http" + "net/url" + "regexp" + "strconv" + "strings" ) var ( eApiKey = "e82ckenh8dichen8" linuxApiKey = "rFgB&h#%2?^eDg:Q" - ///api/song/enhance/player/url - ///eapi/mlivestream/entrance/playlist/get + // /api/song/enhance/player/url + // /eapi/mlivestream/entrance/playlist/get Path = map[string]int{ - "/api/v3/playlist/detail": 1, - "/api/v3/song/detail": 1, - "/api/v6/playlist/detail": 1, - "/api/album/play": 1, - "/api/artist/privilege": 1, - "/api/album/privilege": 1, - "/api/v1/artist": 1, - "/api/v1/artist/songs": 1, - "/api/artist/top/song": 1, - "/api/v1/album": 1, - "/api/album/v3/detail": 1, - "/api/playlist/privilege": 1, - "/api/song/enhance/player/url": 1, - "/api/song/enhance/player/url/v1": 1, - "/api/song/enhance/download/url": 1, - "/batch": 2, //Search - "/api/batch": 1, - "/api/v1/search/get": 2, //IOS - "/api/v1/search/song/get": 2, - "/api/search/complex/get": 1, - "/api/search/complex/get/v2": 2, //Android - "/api/cloudsearch/pc": 3, //PC Value - "/api/v1/playlist/manipulate/tracks": 1, - "/api/song/like": 1, - "/api/v1/play/record": 1, - "/api/playlist/v4/detail": 1, - "/api/v1/radio/get": 1, - "/api/v1/discovery/recommend/songs": 1, - "/api/cloudsearch/get/web": 1, - "/api/song/enhance/privilege": 1, - "/api/osx/version": 1, + "/api/v3/playlist/detail": 1, + "/api/v3/song/detail": 1, + "/api/v6/playlist/detail": 1, + "/api/album/play": 1, + "/api/artist/privilege": 1, + "/api/album/privilege": 1, + "/api/v1/artist": 1, + "/api/v1/artist/songs": 1, + "/api/artist/top/song": 1, + "/api/v1/album": 1, + "/api/album/v3/detail": 1, + "/api/playlist/privilege": 1, + "/api/song/enhance/player/url": 1, + "/api/song/enhance/player/url/v1": 1, + "/api/song/enhance/download/url": 1, + "/batch": 2, // Search + "/api/batch": 1, + "/api/v1/search/get": 2, // IOS + "/api/v1/search/song/get": 2, + "/api/search/complex/get": 1, + "/api/search/complex/get/v2": 2, // Android + "/api/cloudsearch/pc": 3, // PC Value + "/api/v1/playlist/manipulate/tracks": 1, + "/api/song/like": 1, + "/api/v1/play/record": 1, + "/api/playlist/v4/detail": 1, + "/api/v1/radio/get": 1, + "/api/v1/discovery/recommend/songs": 1, + "/api/cloudsearch/get/web": 1, + "/api/song/enhance/privilege": 1, + "/api/osx/version": 1, + "/api/usertool/sound/mobile/promote": 1, + "/api/usertool/sound/mobile/theme": 1, + "/api/usertool/sound/mobile/animationList": 1, + "/api/usertool/sound/mobile/all": 1, + "/api/usertool/sound/mobile/detail": 1, } ) @@ -84,7 +88,7 @@ func RequestBefore(request *http.Request) *Netease { netease := &Netease{Path: request.URL.Path} if request.Method == http.MethodPost && (strings.Contains(netease.Path, "/eapi/") || strings.Contains(netease.Path, "/api/linux/forward")) { - if *config.BlockAds && (strings.Index(netease.Path, "/eapi/ad/") == 0 || strings.Index(netease.Path, "/api/ad/") == 0) { + if *config.BlockAds && (strings.Contains(netease.Path, "api/ad/") || strings.Contains(netease.Path, "api/clientlog/upload") || strings.Contains(netease.Path, "api/feedback/weblog")) { return nil } request.Header.Del("x-napm-retry") @@ -184,8 +188,20 @@ func RequestAfter(request *http.Request, response *http.Response, netease *Netea if ok { code = codeN.String() } - if *config.BlockUpdate && strings.EqualFold(netease.Path, "/api/osx/version") { + if strings.EqualFold(netease.Path, "/api/osx/version") { modified = disableUpdate(netease) + } else if strings.Contains(netease.Path, "/usertool/sound/") { + modified = unblockSoundEffects(netease.JsonBody) + } else if strings.Contains(netease.Path, "/batch") { + modified = localVIP(netease) + for key, resp := range netease.JsonBody { + if strings.Contains(key, "/usertool/sound/") { + modified = unblockSoundEffects(resp.(map[string]interface{})) + } else if *config.BlockAds && strings.Contains(netease.Path, "api/ad/") { + resp = &common.MapType{} + modified = true + } + } } else if !netease.Web && (code == "401" || code == "512") && strings.Contains(netease.Path, "manipulate") { modified = tryCollect(netease, request) } else if !netease.Web && (code == "401" || code == "512") && strings.EqualFold(netease.Path, "/api/song/like") { @@ -200,31 +216,40 @@ func RequestAfter(request *http.Request, response *http.Response, netease *Netea response.Header.Del("transfer-encoding") response.Header.Del("content-encoding") response.Header.Del("content-length") - //netease.JsonBody = netease.JsonBody - //log.Println("NeedRepackage") + // netease.JsonBody = netease.JsonBody + // log.Println("NeedRepackage") modifiedJson, _ := json.Marshal(netease.JsonBody) - //log.Println(netease) - //log.Println(string(modifiedJson)) + // log.Println(netease) + // log.Println(string(modifiedJson)) if netease.Encrypted { modifiedJson = crypto.AesEncryptECB(modifiedJson, []byte(aeskey)) } response.Body = ioutil.NopCloser(bytes.NewBuffer(modifiedJson)) } else { - //log.Println("NotNeedRepackage") + // log.Println("NotNeedRepackage") responseHold := ioutil.NopCloser(bytes.NewBuffer(tmpBody)) response.Body = responseHold } - //log.Println(utils.ToJson(netease.JsonBody)) + + // log.Println("netease.Path: " + netease.Path) + // log.Println("netease.JsonBody: " + utils.ToJson(netease.JsonBody) + + // "\nrequestRequestURI: " + request.RequestURI + + // "\nrequestHeader: " + utils.ToJson(request.Header) + + // "\nrequestMethod: " + request.Method + + // "\nrequestUserAgent: " + request.UserAgent()) } else { responseHold := ioutil.NopCloser(bytes.NewBuffer(tmpBody)) response.Body = responseHold } } else { - //log.Println("Not Process") + // log.Println("Not Process: " + netease.Path) } } func disableUpdate(netease *Netease) bool { + if !*config.BlockUpdate { + return false + } modified := false jsonBody := netease.JsonBody if value, ok := jsonBody["updateFiles"]; ok { @@ -241,10 +266,75 @@ func disableUpdate(netease *Netease) bool { // log.Println(string(modifiedJson)) return modified } +func localVIP(netease *Netease) bool { + if !*config.EnableLocalVip { + return false + } + modified := false + if utils.Exist("/api/music-vip-membership/client/vip/info", netease.JsonBody) { + log.Println("localVIP has been triggered.") + modified = true + info := netease.JsonBody["/api/music-vip-membership/client/vip/info"] + expireTime, _ := info.(common.MapType)["data"].(common.MapType)["now"].(json.Number).Int64() + expireTime += 3162240000000 + info.(common.MapType)["data"].(common.MapType)["redVipLevel"] = 7 + info.(common.MapType)["data"].(common.MapType)["redVipAnnualCount"] = 1 + info.(common.MapType)["data"].(common.MapType)["musicPackage"] = &common.MapType{"expireTime": expireTime, "vipCode": 230} + info.(common.MapType)["data"].(common.MapType)["associator"] = &common.MapType{"expireTime": expireTime} + } + return modified +} +func unblockSoundEffects(jsonBody map[string]interface{}) bool { + if !*config.UnlockSoundEffects { + return false + } + // JsonBody,_ := json.Marshal(jsonBody) + modified := false + if value, ok := jsonBody["data"]; ok { + switch value.(type) { + case common.SliceType: + if len(value.(common.SliceType)) > 0 { + modified = true + for _, data := range value.(common.SliceType) { + if datum, ok := data.(common.MapType); ok { + datum["type"] = 1 + if utils.Exist("type", datum["sound"].(common.MapType)) { + datum["sound"].(common.MapType)["type"] = 1 + } + if utils.Exist("type", datum["animation"].(common.MapType)) { + datum["animation"].(common.MapType)["type"] = 1 + } + } + } + } + case common.MapType: + if utils.Exist("type", value.(common.MapType)) { + modified = true + value.(common.MapType)["type"] = 1 + if utils.Exist("type", value.(common.MapType)["sound"].(common.MapType)) { + value.(common.MapType)["sound"].(common.MapType)["type"] = 1 + } + if utils.Exist("type", value.(common.MapType)["animation"].(common.MapType)) { + value.(common.MapType)["animation"].(common.MapType)["type"] = 1 + } + } + default: + } + } + // modifiedJson, _ := json.Marshal(jsonBody) + // log.Println("netease.JsonBody: " + string(JsonBody)) + // log.Println("netease.modifiedJson: " + string(modifiedJson)) + if modified { + log.Println("unblockSoundEffects has been triggered.") + } + + return modified + +} func tryCollect(netease *Netease, request *http.Request) bool { modified := false - //log.Println(utils.ToJson(netease)) + // log.Println(utils.ToJson(netease)) if utils.Exist("trackIds", netease.Params) { trackId := "" switch netease.Params["trackIds"].(type) { @@ -287,7 +377,7 @@ func tryCollect(netease *Netease, request *http.Request) bool { return modified } func tryLike(netease *Netease, request *http.Request) bool { - //log.Println("try like") + // log.Println("try like") modified := false if utils.Exist("trackId", netease.Params) { trackId := netease.Params["trackId"].(string) @@ -371,8 +461,8 @@ func tryMatch(netease *Netease) bool { default: } } - //modifiedJson, _ := json.Marshal(jsonBody) - //log.Println(string(modifiedJson)) + // modifiedJson, _ := json.Marshal(jsonBody) + // log.Println(string(modifiedJson)) return modified } func searchGreySongs(data common.SliceType, netease *Netease) bool { @@ -423,9 +513,9 @@ func searchGreySong(data common.MapType, netease *Netease) bool { song.Br = 128000 } } - data["encodeType"] = data["type"] //web - data["level"] = "standard" //web - data["fee"] = 8 //web + data["encodeType"] = data["type"] // web + data["level"] = "standard" // web + data["fee"] = 8 // web uri, err := url.Parse(song.Url) if err != nil { log.Println("url.Parse error:", song.Url) @@ -454,7 +544,7 @@ func searchGreySong(data common.MapType, netease *Netease) bool { data["size"] = song.Size data["freeTrialInfo"] = nil data["code"] = 200 - if strings.Contains(netease.Path, "download") { //calculate the file md5 + if strings.Contains(netease.Path, "download") { // calculate the file md5 if !haveSongMd5 { data["md5"] = calculateSongMd5(searchMusic, song.Url) } @@ -465,7 +555,7 @@ func searchGreySong(data common.MapType, netease *Netease) bool { } else { } - //log.Println(utils.ToJson(data)) + // log.Println(utils.ToJson(data)) return modified } func calculateSongMd5(music common.SearchMusic, songUrl string) string { @@ -489,7 +579,7 @@ func calculateSongMd5(music common.SearchMusic, songUrl string) string { } songMd5 = hex.EncodeToString(h.Sum(nil)) provider.UpdateCacheMd5(music, songMd5) - //log.Println("calculateSongMd5 songId:", songId, ",songUrl:", songUrl, ",md5:", songMd5) + // log.Println("calculateSongMd5 songId:", songId, ",songUrl:", songUrl, ",md5:", songMd5) return songMd5 } func processSliceJson(jsonSlice common.SliceType) bool { @@ -503,8 +593,8 @@ func processSliceJson(jsonSlice common.SliceType) bool { needModify = processSliceJson(value.(common.SliceType)) || needModify default: - //log.Printf("index(%T):%v\n", index, index) - //log.Printf("value(%T):%v\n", value, value) + // log.Printf("index(%T):%v\n", index, index) + // log.Printf("value(%T):%v\n", value, value) } } return needModify @@ -513,7 +603,7 @@ func processMapJson(jsonMap common.MapType) bool { needModify := false if utils.Exists([]string{"st", "subp", "pl", "dl"}, jsonMap) { if v, _ := jsonMap["st"]; v.(json.Number).String() != "0" { - //open gray song + // open gray song jsonMap["st"] = 0 needModify = true } @@ -537,7 +627,7 @@ func processMapJson(jsonMap common.MapType) bool { case common.SliceType: needModify = processSliceJson(value.(common.SliceType)) || needModify default: - //if key == "fee" { + // if key == "fee" { // fee := "0" // switch value.(type) { // case int: @@ -551,14 +641,14 @@ func processMapJson(jsonMap common.MapType) bool { // jsonMap[key] = 0 // needModify = true // } - //} + // } } } return needModify } func unifiedMusicQuality(netease *Netease) { - //log.Println(fmt.Sprintf("%+v\n", utils.ToJson(netease.Params))) + // log.Println(fmt.Sprintf("%+v\n", utils.ToJson(netease.Params))) netease.MusicQuality = common.Lossless if !*config.ForceBestQuality { if levelParam, ok := netease.Params["level"]; ok { @@ -588,7 +678,7 @@ func unifiedMusicQuality(netease *Netease) { } } } - //log.Println(fmt.Sprintf("%+v\n", utils.ToJson(netease.MusicQuality))) + // log.Println(fmt.Sprintf("%+v\n", utils.ToJson(netease.MusicQuality))) } } func generateEndpoint(netease *Netease) string { @@ -616,24 +706,24 @@ func generateEndpoint(netease *Netease) string { } } netease.EndPoint = protocol + endPoint - //log.Println(fmt.Sprintf("%+v\n", utils.ToJson(netease.EndPoint))) + // log.Println(fmt.Sprintf("%+v\n", utils.ToJson(netease.EndPoint))) return netease.EndPoint } func searchOtherPlatform(netease *Netease) *Netease { - //log.Println(utils.ToJson(netease)) + // log.Println(utils.ToJson(netease)) if *config.SearchLimit > 0 && Path[netease.Path] == 2 { var paramsMap map[string]interface{} - if utils.Exists([]string{"offset", "s"}, netease.Params) { //单曲 + if utils.Exists([]string{"offset", "s"}, netease.Params) { // 单曲 netease.SearchPath = netease.Path paramsMap = netease.Params - } else if utils.Exists([]string{"keyword", "scene"}, netease.Params) { //综合 + } else if utils.Exists([]string{"keyword", "scene"}, netease.Params) { // 综合 netease.SearchPath = netease.Path paramsMap = netease.Params - } else { //pc + } else { // pc for k, v := range netease.Params { - //pc + // pc if t, ok := Path[k]; ok { - if t == 3 { //search + if t == 3 { // search if searchValue, ok := v.(string); ok { paramsMap = utils.ParseJson([]byte(searchValue)) netease.SearchPath = k @@ -646,13 +736,13 @@ func searchOtherPlatform(netease *Netease) *Netease { if paramsMap != nil { var offset int64 if offsetJson, ok := paramsMap["offset"].(json.Number); ok { - //just offset=0 + // just offset=0 if i, err := offsetJson.Int64(); err == nil { offset = i } } else if offsetS, ok := paramsMap["offset"].(string); ok { - //just offset=0 + // just offset=0 if i, err := strconv.ParseInt(offsetS, 10, 64); err == nil { offset = i } @@ -681,7 +771,7 @@ func searchOtherPlatform(netease *Netease) *Netease { } func trySearch(netease *Netease, ch chan []*common.Song) { ch <- provider.SearchSongFromAllSource(common.SearchSong{Keyword: netease.SearchKey, Limit: *config.SearchLimit, OrderBy: common.PlatformDefault}) - //log.Println(utils.ToJson(songs)) + // log.Println(utils.ToJson(songs)) } func tryAddOtherPlatformResult(netease *Netease) bool { modified := false @@ -694,7 +784,7 @@ func tryAddOtherPlatformResult(netease *Netease) bool { close(netease.SearchTaskChan) } } - if len(netease.SearchKey) > 0 && netease.JsonBody != nil && len(netease.SearchSongs) > 0 { //搜索页面 + if len(netease.SearchKey) > 0 && netease.JsonBody != nil && len(netease.SearchSongs) > 0 { // 搜索页面 var orginalMap common.MapType var orginalSongsKey string var neteaseSongs common.SliceType @@ -707,7 +797,7 @@ func tryAddOtherPlatformResult(netease *Netease) bool { } } - } else if jBody, ok := netease.JsonBody["data"].(common.MapType); ok { //android 综合 + } else if jBody, ok := netease.JsonBody["data"].(common.MapType); ok { // android 综合 if result, ok := jBody["complete"].(common.MapType); ok { if s, ok := result["song"].(common.MapType); ok { if neteaseSongs, ok = s["songs"].(common.SliceType); ok { @@ -719,10 +809,10 @@ func tryAddOtherPlatformResult(netease *Netease) bool { } } else if jBody, ok := netease.JsonBody["result"].(common.MapType); ok { - if neteaseSongs, ok = jBody["songs"].(common.SliceType); ok { //android&ios 单曲 + if neteaseSongs, ok = jBody["songs"].(common.SliceType); ok { // android&ios 单曲 orginalMap = jBody orginalSongsKey = "songs" - } else if s, ok := jBody["song"].(common.MapType); ok { //ios 综合 + } else if s, ok := jBody["song"].(common.MapType); ok { // ios 综合 if neteaseSongs, ok = s["songs"].(common.SliceType); ok { orginalMap = s orginalSongsKey = "songs" @@ -757,7 +847,7 @@ func tryAddOtherPlatformResult(netease *Netease) bool { idTag = common.KuWoTag } - if _, ok := copySong["name"]; ok { //make sure ok + if _, ok := copySong["name"]; ok { // make sure ok copySong["alia"] = []string{source} if ar, ok := copySong["ar"]; ok { artMap := make(common.MapType) diff --git a/provider/provider.go b/provider/provider.go index 1f2c587..e1b2cd7 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -10,8 +10,7 @@ import ( kugou "github.com/cnsilvan/UnblockNeteaseMusic/provider/kugou" "github.com/cnsilvan/UnblockNeteaseMusic/provider/kuwo" "github.com/cnsilvan/UnblockNeteaseMusic/provider/migu" - - //"github.com/cnsilvan/UnblockNeteaseMusic/provider/qq" + "github.com/cnsilvan/UnblockNeteaseMusic/provider/qq" "net/http" "net/url" "strconv" @@ -44,6 +43,8 @@ func NewProvider(kind string) Provider { return &kugou.KuGou{} case "migu": return &migu.Migu{} + case "qq": + return &qq.QQ{} default: return &kuwo.KuWo{} } @@ -86,6 +87,8 @@ func Find(music common.SearchMusic) common.Song { re = calculateSongInfo(GetProvider("kuwo").GetSongUrl(music, song)) } else if strings.Index(music.Id, string(common.MiGuTag)) == 0 { re = calculateSongInfo(GetProvider("migu").GetSongUrl(music, song)) + } else if strings.Index(music.Id, string(common.QQTag)) == 0 { + re = calculateSongInfo(GetProvider("qq").GetSongUrl(music, song)) } else { } diff --git a/provider/qq/qq.go b/provider/qq/qq.go new file mode 100644 index 0000000..b3c43a6 --- /dev/null +++ b/provider/qq/qq.go @@ -0,0 +1,245 @@ +package qq + +import ( + "github.com/cnsilvan/UnblockNeteaseMusic/common" + "github.com/cnsilvan/UnblockNeteaseMusic/config" + "github.com/cnsilvan/UnblockNeteaseMusic/network" + "github.com/cnsilvan/UnblockNeteaseMusic/provider/base" + "github.com/cnsilvan/UnblockNeteaseMusic/utils" + "github.com/mitchellh/mapstructure" + "log" + "math/rand" + "net/http" + "net/url" + "os" + "strconv" + "strings" + "sync" + "time" +) + +type QQ struct{} + +type typeSong struct { + Album struct { + Name string + } + File struct { + Media_Mid string + } + Mid string + Name string + Singer []struct { + Name string + } +} + +type typeSongResult struct { + Code int + Data struct { + Sip []string + Midurlinfo []struct { + Purl string + } + } +} + +type getSongConfig struct { + fmid string + mid string + cookies []*http.Cookie + song *common.Song + br string + format string +} + +func (m *QQ) SearchSong(song common.SearchSong) (songs []*common.Song) { + song = base.PreSearchSong(song) + cookies := getCookies() + result, err := base.Fetch( + "https://c.y.qq.com/soso/fcgi-bin/client_search_cp?"+ + "ct=24&qqmusic_ver=1298&new_json=1&remoteplace=txt.yqq.center&"+ + "t=0&aggr=1&cr=1&catZhida=1&lossless=0&flag_qc=0&p=1&n=20&w="+ + song.Keyword+ + "&"+ + "g_tk=5381&loginUin=0&hostUin=0&"+ + "format=json&inCharset=utf8&outCharset=utf-8¬ice=0&platform=yqq&needNewCode=0", + cookies, nil, true) + if err != nil { + log.Println(err) + return songs + } + data := result["data"] + if data != nil { + if dMap, ok := data.(common.MapType); ok { + if dSong, ok := dMap["song"].(common.MapType); ok { + if list, ok := dSong["list"].(common.SliceType); ok { + if ok && len(list) > 0 { + listLength := len(list) + maxIndex := listLength/2 + 1 + if maxIndex > 10 { + maxIndex = 10 + } + for index, matched := range list { + if index >= maxIndex { + break + } + + qqSong := &typeSong{} + if err = mapstructure.Decode(matched, &qqSong); err == nil { + artists := make([]string, 2) + for _, singer := range qqSong.Singer { + artists = append(artists, singer.Name) + } + songResult := &common.Song{} + songResult.PlatformUniqueKey = matched.(common.MapType) + songResult.PlatformUniqueKey["UnKeyWord"] = song.Keyword + songResult.PlatformUniqueKey["Mid"] = qqSong.File.Media_Mid + songResult.PlatformUniqueKey["MusicId"] = qqSong.Mid + songResult.Source = "qq" + songResult.Name = qqSong.Name + songResult.Artist = strings.Join(artists, " & ") + songResult.AlbumName = qqSong.Album.Name + songResult.Id = string(common.QQTag) + qqSong.Mid + songResult.MatchScore, ok = base.CalScore(song, qqSong.Name, songResult.Artist, index, maxIndex) + if !ok { + continue + } + songs = append(songs, songResult) + } + } + } + } + } + } + } + return base.AfterSearchSong(song, songs) +} + +func (m *QQ) GetSongUrl(searchSong common.SearchMusic, song *common.Song) *common.Song { + if fmid, ok := song.PlatformUniqueKey["Mid"].(string); ok { + if mid, ok := song.PlatformUniqueKey["MusicId"].(string); ok { + cookies := getCookies() + if cookies == nil { + format := "mp3" + br := "M800" + searchSong.Quality = common.Standard + conf := &getSongConfig{ + fmid, + mid, + nil, + song, + br, + format, + } + if gotSong := getSong(conf); gotSong != nil { + song = gotSong + } + } else { + wg := sync.WaitGroup{} + wg.Add(3) + rand.Seed(time.Now().UnixNano()) + songCh := make(chan *common.Song, 3) + for _, quality := range []map[string]string{{"M500": "mp3"}, {"M800": "mp3"}, {"F000": "flac"}} { + for br, format := range quality { + conf := &getSongConfig{ + fmid, + mid, + cookies, + song, + br, + format, + } + go func(conf *getSongConfig) { + if gotSong := getSong(conf); gotSong != nil { + gotSong.PlatformUniqueKey["Quality"] = conf.br + songCh <- gotSong + } + wg.Done() + }(conf) + } + } + wg.Wait() + songs := make(map[string]*common.Song, 3) + for gotSong := range songCh { + songs[gotSong.PlatformUniqueKey["Quality"].(string)] = gotSong + if len(songCh) == 0 { + break + } + } + quality := "M500" + finished := false + for !finished { + switch searchSong.Quality { + case common.Standard: + quality = "M500" + case common.Higher: + fallthrough + case common.ExHigh: + quality = "M800" + case common.Lossless: + quality = "F000" + default: + quality = "M500" + } + if gotSong, ok := songs[quality]; ok { + song = gotSong + finished = true + } + searchSong.Quality-- + } + } + } + } + return song +} + +func getSong(config *getSongConfig) *common.Song { + guid := utils.ToFixed(rand.Float64()*10000000, 0) + rawQueryData := `{"req_0":{"module":"vkey.GetVkeyServer","method":"CgiGetVkey","param":{"guid":"` + + strconv.Itoa(int(guid)) + + `","loginflag":1,"filename":["` + config.br + config.fmid + "." + config.format + + `"],"songmid":["` + config.mid + `"],"songtype":[0],"uin":"0","platform":"20"}}}` + clientRequest := network.ClientRequest{ + Method: http.MethodGet, + ForbiddenEncodeQuery: true, + RemoteUrl: "https://u.y.qq.com/cgi-bin/musicu.fcg?data=" + url.QueryEscape(rawQueryData), + Proxy: true, + Cookies: config.cookies, + } + resp, err := network.Request(&clientRequest) + if err != nil { + log.Println(err) + } + defer resp.Body.Close() + body, err := network.StealResponseBody(resp) + songData := utils.ParseJsonV2(body) + songResult := &typeSongResult{} + if err = mapstructure.Decode(songData["req_0"], &songResult); err == nil { + if songResult.Data.Midurlinfo[0].Purl != "" { + config.song.Url = songResult.Data.Sip[0] + songResult.Data.Midurlinfo[0].Purl + return config.song + } else { + log.Println(config.song.PlatformUniqueKey["UnKeyWord"].(string) + ",该歌曲QQ音乐版权保护") + // log.Println(utils.ToJson(songData)) + } + } + + return nil +} + +func (m *QQ) ParseSong(searchSong common.SearchSong) *common.Song { + song := &common.Song{} + songs := m.SearchSong(searchSong) + if len(songs) > 0 { + song = m.GetSongUrl(common.SearchMusic{Quality: searchSong.Quality}, songs[0]) + } + return song +} + +func getCookies() []*http.Cookie { + if _, err := os.Stat(*config.QQCookieFile); os.IsNotExist(err) { + return nil + } + return utils.ParseCookies(*config.QQCookieFile) +} diff --git a/utils/utils.go b/utils/utils.go index 99e6698..aacc6b5 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -1,6 +1,7 @@ package utils import ( + "bufio" "bytes" "compress/gzip" "crypto/md5" @@ -9,9 +10,12 @@ import ( "encoding/json" "errors" "fmt" + "github.com/cnsilvan/UnblockNeteaseMusic/cookiestxt" "io" "io/ioutil" "log" + "math" + "net/http" "os" "os/exec" "path/filepath" @@ -302,3 +306,27 @@ func ParseSingerKeyWord(data string) []string { sort.Sort(ByLenSort(keys)) return keys } + +func round(num float64) int { + return int(num + math.Copysign(0.5, num)) +} + +func ToFixed(num float64, precision int) float64 { + output := math.Pow(10, float64(precision)) + return float64(round(num*output)) / output +} + +func ParseCookies(file string) []*http.Cookie { + fl, err := os.Open(file) + if err != nil { + fmt.Println(file, err) + return nil + } + defer fl.Close() + r := bufio.NewReader(fl) + cl, err := cookiestxt.Parse(r) + if err != nil { + return nil + } + return cl +}