Repository: WhiteBlue/bilibili-sdk-go Branch: master Commit: ec7097f5828e Files: 28 Total size: 61.4 KB Directory structure: gitextract_t4bibrws/ ├── .gitignore ├── Dockerfile ├── Godeps/ │ ├── Godeps.json │ └── Readme ├── LICENSE ├── README.md ├── client/ │ ├── bangumi.go │ ├── base.go │ ├── cli.go │ ├── others.go │ ├── rank.go │ ├── special.go │ ├── user.go │ ├── utils.go │ └── video.go ├── conf.example.json ├── conf.json ├── deploy.sh ├── docs/ │ └── api_doc.md ├── main.go ├── run.sh ├── service/ │ ├── application.go │ ├── cache.go │ ├── config.go │ ├── corn.go │ ├── corn_tasks.go │ └── router.go └── test/ └── api_test.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a *.so # Folders _obj _test # Architecture specific extensions/prefixes *.[568vq] [568vq].out *.cgo1.go *.cgo2.c _cgo_defun.c _cgo_gotypes.go _cgo_export.* _testmain.go *.exe *.test *.prof *.iml .idea test.go bilibili-service ================================================ FILE: Dockerfile ================================================ FROM golang:onbuild MAINTAINER whiteblue0616@gmail.com ADD . $GOPATH/src/github.com/whiteblue/bilibili-go WORKDIR $GOPATH/src/github.com/whiteblue/bilibili-go # RUN go get -u github.com/go-playground/log \ # && go get -u github.com/gin-gonic/gin \ # && go get -u github.com/gin-gonic/contrib/gzip \ # && go get -u github.com/valyala/fasthttp RUN go get github.com/tools/godep EXPOSE 8080 CMD ["./run.sh"] ================================================ FILE: Godeps/Godeps.json ================================================ { "ImportPath": "github.com/whiteblue/bilibili-go", "GoVersion": "go1.7", "GodepVersion": "v75", "Deps": [ { "ImportPath": "github.com/anacrolix/missinggo", "Rev": "03b41562f79c09a2bccb139f2a6282dcd14d6ce6" }, { "ImportPath": "github.com/anacrolix/sync", "Rev": "812602587b72df6a2a4f6e30536adc75394a374b" }, { "ImportPath": "github.com/gin-gonic/contrib/gzip", "Rev": "547e518040cfb96576b507d2f26779ab9c6fc829" }, { "ImportPath": "github.com/gin-gonic/gin", "Comment": "v1.0rc1-268-gf931d1e", "Rev": "f931d1ea80ae95a6fc739213cdd9399bd2967fb6" }, { "ImportPath": "github.com/gin-gonic/gin/binding", "Comment": "v1.0rc1-268-gf931d1e", "Rev": "f931d1ea80ae95a6fc739213cdd9399bd2967fb6" }, { "ImportPath": "github.com/gin-gonic/gin/render", "Comment": "v1.0rc1-268-gf931d1e", "Rev": "f931d1ea80ae95a6fc739213cdd9399bd2967fb6" }, { "ImportPath": "github.com/go-playground/log", "Comment": "v4.0.1", "Rev": "700a09cca964de69c81ed1d7fa3bb9b94af1fd5f" }, { "ImportPath": "github.com/go-playground/log/handlers/console", "Comment": "v4.0.1", "Rev": "700a09cca964de69c81ed1d7fa3bb9b94af1fd5f" }, { "ImportPath": "github.com/golang/protobuf/proto", "Rev": "1f49d83d9aa00e6ce4fc8258c71cc7786aec968a" }, { "ImportPath": "github.com/klauspost/compress/flate", "Comment": "v1.1-2-ge3b7981", "Rev": "e3b7981a12dd3cab49afa1d3a50e715846f23732" }, { "ImportPath": "github.com/klauspost/compress/gzip", "Comment": "v1.1-2-ge3b7981", "Rev": "e3b7981a12dd3cab49afa1d3a50e715846f23732" }, { "ImportPath": "github.com/klauspost/compress/zlib", "Comment": "v1.1-2-ge3b7981", "Rev": "e3b7981a12dd3cab49afa1d3a50e715846f23732" }, { "ImportPath": "github.com/klauspost/cpuid", "Comment": "v1.0", "Rev": "09cded8978dc9e80714c4d85b0322337b0a1e5e0" }, { "ImportPath": "github.com/klauspost/crc32", "Comment": "v1.0-2-gcb6bfca", "Rev": "cb6bfca970f6908083f26f39a79009d608efd5cd" }, { "ImportPath": "github.com/manucorporat/sse", "Rev": "ee05b128a739a0fb76c7ebd3ae4810c1de808d6d" }, { "ImportPath": "github.com/valyala/bytebufferpool", "Rev": "e746df99fe4a3986f4d4f79e13c1e0117ce9c2f7" }, { "ImportPath": "github.com/valyala/fasthttp", "Comment": "v20160617-69-g6cd438b", "Rev": "6cd438ba896c48caa4194fb7381d91eae33b9461" }, { "ImportPath": "github.com/valyala/fasthttp/fasthttputil", "Comment": "v20160617-69-g6cd438b", "Rev": "6cd438ba896c48caa4194fb7381d91eae33b9461" }, { "ImportPath": "github.com/valyala/fasthttp/stackless", "Comment": "v20160617-69-g6cd438b", "Rev": "6cd438ba896c48caa4194fb7381d91eae33b9461" }, { "ImportPath": "golang.org/x/net/context", "Rev": "6250b412798208e6c90b03b7c4f226de5aa299e2" }, { "ImportPath": "gopkg.in/go-playground/validator.v8", "Comment": "v8.18.1", "Rev": "5f57d2222ad794d0dffb07e664ea05e2ee07d60c" }, { "ImportPath": "gopkg.in/yaml.v2", "Rev": "e4d366fc3c7938e2958e662b4258c7a89e1f0e3e" } ] } ================================================ FILE: Godeps/Readme ================================================ This directory tree is generated automatically by godep. Please do not edit. See https://github.com/tools/godep for more information. ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2015 WhiteBlue Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # bilibili-sdk-go BiliBili Open API & SDK written in Go ## Open API Docs: [docs](docs/api_doc.md) * api.bilibilih5.club * api.prprpr.me/bilibili ( support by [DIYgod](https://github.com/DIYgod)) Deploy: * ```docker build -t bilibili-go``` * ```docekr run -d -p 80:8080 bilibili-go``` ## Progress * Rank * ```SortRank``` (order by danmu/comment/hot) * Video * ```VideoInfo``` * ```VideoLin``` (mp4/flv) * User * ```UserInfo``` * ```UserVideos``` * Special * ```SpecialInfo``` * ```SpecialVideos``` * Bangumi * ```BangumiList``` * ```BangumiRecommend``` * Others * ```Search```(search user/video/bangumi) ## Install ``` go get github.com/WhiteBlue/bilibili-go ``` ## Usage ``` import "github.com/whiteblue/bilibili-go/client" c := client.NewClient("APPKEY", "SECRET") back, err := c.Bangumi.GetWeekList("2") if err != nil { log.Error(err) return } log.Info(result) ``` ## Related Projects * [BiliBili-Html5](http://bilibilih5.club) ## License MIT License Copyright (c) 2016 Castaway Consulting LLC Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: client/bangumi.go ================================================ package client import ( "encoding/json" ) type bangumiElement struct { Title string `json:"title"` Area string `json:"area"` AreaLimit int `json:"arealimit"` Attention int `json:"attention"` BangumiId int `json:"bangumi_id"` BgmCount string `json:"bgmcount"` Cover string `json:"cover"` SquareCover string `json:"square_cover"` DanmakuCount int `json:"danmaku_count"` Favorites int `json:"favorites"` IsFinish int `json:"is_finish"` LastUpdate string `json:"lastupdate_at"` New bool `json:"new"` PlayCount int `json:"play_count"` SeasonId int `json:"season_id"` SpId int `json:"spid"` Url string `json:"url"` ViewRank int `json:"viewRank"` Weekday int `json:"weekday"` } type banner struct { Title string `json:"title"` Link string `json:"link"` Img string `json:"img"` SImg string `json:"simg"` Aid int `json:"aid"` Type string `json:"type"` Platform int `json:"platform"` Pid int `json:"pid"` } type recommendBangumiVideo struct { Aid string `json:"aid"` Title string `json:"title"` Subtitle string `json:"subtitle"` Play int `json:"play"` Review int `json:"review"` VideoReview int `json:"video_review"` Favorites int `json:"favorites"` Mid int `json:"mid"` Author string `json:"author"` Description string `json:"description"` Create string `json:"create"` Pic string `json:"pic"` Coins int `json:"coins"` Duration string `json:"duration"` } type bangumiActor struct { Actor string `json:"actor"` Role string `json:"role"` } type bangumiVideo struct { Aid string `json:"av_id"` Coins int `json:"coins"` Cover string `json:"cover"` Danmaku string `json:"danmaku"` Index string `json:"index"` Title string `json:"index_title"` UpdateTime string `json:"update_time"` } type bangumiSeason struct { Cover string `json:"cover"` IsFinish string `json:"is_finish"` SeasonId string `json:"season_id"` SeasonStatus int `json:"season_status"` Title string `json:"title"` TotalCount string `json:"total_count"` } type bangumiInfoResponse struct { Actors []bangumiActor `json:"actor"` Alias string `json:"alias"` Area string `json:"area"` BangumiId string `json:"bangumi_id"` BangumiTitle string `json:"bangumi_title"` Brief string `json:"brief"` Coins string `json:"coins"` CopyRight string `json:"copyright"` Cover string `json:"cover"` DanmakuCount string `json:"danmaku_count"` Episodes []bangumiVideo `json:"episodes"` Evaluate string `json:"evaluate"` Favorites string `json:"favorites"` IsFinish string `json:"is_finish"` JpTitle string `json:"jp_title"` PlayCount string `json:"play_count"` PubTime string `json:"pub_time"` SeasonId string `json:"season_id"` SeasonStatus int `json:"season_status"` SeasonTitle string `json:"season_title"` Seasons []bangumiSeason `json:"seasons"` SquareCover string `json:"squareCover"` Staff string `json:"staff"` Title string `json:"title"` TotalCount string `json:"total_count"` } type weekBangumiResponse struct { Count string `json:"count"` List []bangumiElement `json:"list"` } type bangumiIndexResponse struct { Banners []banner `json:"banners"` Recommends []recommendBangumiVideo `json:"recommends"` } type BangumiService struct { BaseService } func (b *BangumiService) GetWeekList(bType string) (*weekBangumiResponse, error) { retBody, err := b.doRequest("http://app.bilibili.com/bangumi/timeline_v2", map[string]string{ "_device": "iphone", "btype": bType, "platform": "ios", "type": "json", }) if err != nil { return nil, err } var ret weekBangumiResponse json.Unmarshal(retBody, &ret) return &ret, nil } func (b *BangumiService) GetIndex() (*bangumiIndexResponse, error) { retBody, err := b.doRequest("http://app.bilibili.com/api/region_ios/13.json", map[string]string{ "platform": "ios", "device": "phone", }) if err != nil { return nil, err } var ret struct { Content bangumiIndexResponse `json:"result"` } json.Unmarshal(retBody, &ret) return &ret.Content, nil } func (b *BangumiService) GetBangumiInfo(seasonId string) (*bangumiInfoResponse, error) { retBody, err := b.doRequest("http://bangumi.bilibili.com/api/season_v4", map[string]string{ "platform": "ios", "build": "3940", "season_id": seasonId, "type": "bangumi", }) if err != nil { return nil, err } var ret struct { Content bangumiInfoResponse `json:"result"` } json.Unmarshal(retBody, &ret) return &ret.Content, nil } ================================================ FILE: client/base.go ================================================ package client import ( "encoding/json" ) type BaseParam struct { Appkey string Secret string } type BaseService struct { Params BaseParam Client HttpClient } type apiResponse struct { Code int `json:"code"` Message string `json:"message"` Error string `json:"error"` } type ApiError struct { Msg string } func (a *ApiError) Error() string { return a.Msg } func (b *BaseService) doRequest(url string, params map[string]string) ([]byte, error) { params["appkey"] = b.Params.Appkey //generate bilibili sign code query, sign := EncodeSign(params, b.Params.Secret) reqUrl := url + "?" + query + "&sign=" + sign retByte, err := b.Client.Get(reqUrl) if err != nil { return nil, err } var badRet apiResponse err = json.Unmarshal(retByte, &badRet) if err != nil { return nil, &ApiError{Msg: "api encode error"} } if badRet.Code == 0 { //api return success return retByte, nil } //api return error return nil, &ApiError{Msg: badRet.Message + badRet.Error} } ================================================ FILE: client/cli.go ================================================ package client type BCli struct { Rank RankService Bangumi BangumiService Video VideoService Special SpecialService User UserService Others OthersService } func NewClient(appkey, secret string) *BCli { params := BaseParam{ Appkey: appkey, Secret: secret, } client := NewHttpClient() base := BaseService{params, client} return &BCli{ Rank: RankService{base}, Bangumi: BangumiService{base}, Video: VideoService{base}, Special: SpecialService{base}, User: UserService{base}, Others: OthersService{base}, } } ================================================ FILE: client/others.go ================================================ package client import ( "encoding/json" "net/url" "strconv" "strings" ) type videoTypeInfoElement struct { Tid int `json:"tid"` Name string `json:"name"` Count int `json:"count"` } type searchItem struct { Title string `json:"title"` Cover string `json:"cover"` Uri string `json:"uri"` Params string `json:"param"` Goto string `json:"goto"` Desc string `json:"desc"` MovieActors string `json:"actors"` MovieStaff string `json:"staff"` MoviePubDate string `json:"screen_date"` MovieArea string `json:"area"` MovieLength int `json:"length"` VideoPlay int `json:"play"` VideoDanmaku int `json:"danmaku"` VideoAuthor string `json:"author"` VideoDuration string `json:"duration"` SeasonTotalCount int `json:"total_count"` SeasonDesc string `json:"cat_desc"` UserFans int `json:"fans"` UserSign string `json:"sign"` } type searchItems struct { Seasons []searchItem `json:"season"` Movies []searchItem `json:"movie"` Vides []searchItem `json:"archive"` } type searchNavItem struct { Name string `json:"name"` Total int `json:"total"` Pages int `json:"pages"` Type int `json:"type"` } type searchResponse struct { Page int `json:"page"` Navs []searchNavItem `json:"nav"` Items searchItems `json:"items"` } type searchByTypeResponse struct { AllPage int `json:"pages"` Items []searchItem `json:"items"` } type BannerElement struct { Id int `json:"id"` Name string `json:"name"` Pic string `json:"pic"` Url string `json:"url"` PosNum int `json:"pos_num"` } type liveBanner struct { Title string `json:"title"` Img string `json:"img"` Remark string `json:"remark"` Link string `json:"link"` } type liveElement struct { User struct { Face string `json:"face"` Mid int `json:"mid"` Name string `json:"name"` } `json:"owner"` Cover struct { Src string `json:"src"` } `json:"cover"` Title string `json:"title"` RoomId int `json:"room_id"` Online int `json:"online"` Area string `json:"area"` AreaId int `json:"area_id"` PlayUrl string `json:"playurl"` AcceptQuality string `json:"accept_quality"` } type liveAppIndexResponse struct { Banners []liveBanner `json:"banner"` Partitions []struct { Partition struct { Id int `json:"id"` Name string `json:"name"` Area string `json:"area"` SubIcon struct { Src string `json:"src"` } `json:"sub_icon"` } `json:"partition"` Lives []liveElement `json:"lives"` } `json:"partitions"` Recommend struct { Lives []liveElement `json:"lives"` BannerData []liveElement `json:"banner_data"` } `json:"recommend_data"` } type OthersService struct { BaseService } /* order: "totalrank" "click" "pubdate" "dm" searchType: "all" */ func (o *OthersService) Search(keyword string, page, pageSize int, order string) (*searchResponse, error) { //url raw encode keywordEncode := strings.Replace(url.QueryEscape(keyword), "+", "%20", -1) retBody, err := o.doRequest("http://app.bilibili.com/x/v2/search", map[string]string{ "keyword": keywordEncode, "pn": strconv.Itoa(page), "ps": strconv.Itoa(pageSize), "device": "phone", "main_ver": "v3", "order": order, "platform": "ios", }) if err != nil { return nil, err } var ret struct { Data searchResponse `json:"data"` } json.Unmarshal(retBody, &ret) return &ret.Data, nil } func (o *OthersService) SearchByType(keyword string, page, pageSize int, searchType int) (*searchByTypeResponse, error) { //url raw encode keywordEncode := strings.Replace(url.QueryEscape(keyword), "+", "%20", -1) retBody, err := o.doRequest("http://app.bilibili.com/x/v2/search/type", map[string]string{ "keyword": keywordEncode, "pn": strconv.Itoa(page), "ps": strconv.Itoa(pageSize), "mobi_app": "iphone", "platform": "ios", "device": "phone", "type": strconv.Itoa(searchType), }) if err != nil { return nil, err } var ret struct { Data searchByTypeResponse `json:"data"` } json.Unmarshal(retBody, &ret) return &ret.Data, nil } func (o *OthersService) AppIndex() (*liveAppIndexResponse, error) { retBody, err := o.doRequest("http://live.bilibili.com/AppIndex/home", map[string]string{ "device": "phone", "platform": "ios", "scale": "2", "actionKey": "appkey", }) if err != nil { return nil, err } var ret struct { Data liveAppIndexResponse `json:"data"` } json.Unmarshal(retBody, &ret) return &ret.Data, nil } func (o *OthersService) IndexBanner() ([]BannerElement, error) { retBody, err := o.doRequest("http://api.bilibili.com/x/web-show/res/loc", map[string]string{ "jsonp":"jsonp", "pf":"0", "id":"23", }) if err != nil { return nil, err } var ret struct { Data []BannerElement `json:"data"` } json.Unmarshal(retBody, &ret) return ret.Data, nil } ================================================ FILE: client/rank.go ================================================ package client import ( "encoding/json" "strconv" ) type RankService struct { BaseService } type RankVideoElement struct { Title string `json:"title"` Cover string `json:"cover"` Uri string `json:"uri"` Param string `json:"param"` Goto string `json:"goto"` Name string `json:"name"` Play int `json:"play"` Reply int `json:"reply"` Favourite int `json:"favourite"` Danmaku int `json:"danmaku"` } /* order: "view", "senddate", "reply", "danmaku", "favorite", */ func (r *RankService) SortRank(tid, page, pageSize int, order string) ([]RankVideoElement, error) { retBody, err := r.doRequest("http://app.bilibili.com/x/v2/region/show/child/list", map[string]string{ "build": "4040", "device": "phone", "mobi_app": "iphone", "platform": "ios", "order": order, "pn": strconv.Itoa(page), "ps": strconv.Itoa(pageSize), "rid": strconv.Itoa(tid), }) if err != nil { return nil, err } var ret struct { List []RankVideoElement `json:"data"` } //delete the 'num' key (mdzz) json.Unmarshal(retBody, &ret) return ret.List, nil } ================================================ FILE: client/special.go ================================================ package client import ( "encoding/json" "strconv" ) type specialVideoElement struct { Aid int `json:"aid"` Cid int `json:"cid"` Cover string `json:"cover"` Title string `json:"title"` Click int `json:"click"` Page int `json:"page"` } type specialVideosResponse struct { Count int `json:"count"` Results int `json:"results"` List []specialVideoElement `json:"list"` } type specialInfoResponse struct { SpId int `json:"spid"` Title string `json:"title"` CreateAt string `json:"create_at"` UpdateAt string `json:"lastupdate_at"` Alias string `json:"alias"` Cover string `json:"cover"` IsBangumi int `json:"isbangumi"` IsBangumiEnd int `json:"isbangumi_end"` BangumiDate string `json:"bangumi_date"` Description string `json:"description"` View int `json:"view"` VideoView int `json:"video_view"` Favourite int `json:"favourite"` Attention int `json:"attention"` } type SpecialService struct { BaseService } func (s *SpecialService) GetSpecialInfo(spid int) (*specialInfoResponse, error) { retBody, err := s.doRequest("http://api.bilibili.cn/sp", map[string]string{ "spid": strconv.Itoa(spid), }) if err != nil { return nil, err } var ret specialInfoResponse json.Unmarshal(retBody, &ret) return &ret, nil } /* isBangumi: the result is "bangumi" or other videos */ func (s *SpecialService) GetSpecialVideos(spid int, isBangumi bool) (*specialVideosResponse, error) { retType := 0 if isBangumi { retType = 1 } retBody, err := s.doRequest("http://api.bilibili.com/spview", map[string]string{ "spid": strconv.Itoa(spid), "bangumi": strconv.Itoa(retType), "type": "json", }) if err != nil { return nil, err } var ret specialVideosResponse json.Unmarshal(retBody, &ret) return &ret, nil } ================================================ FILE: client/user.go ================================================ package client import ( "encoding/json" "strconv" ) type userVideosResponse struct { List []UserVideoElement `json:"vlist"` TypeIndex map[string]videoTypeInfoElement `json:"tlist"` } type UserVideoElement struct { Aid int `json:"aid"` Copyright string `json:"copyright"` TypeId int `json:"typeid"` Title string `json:"title"` Subtitle string `json:"subtitle"` Play int `json:"play"` Review int `json:"review"` VideoReview int `json:"video_review"` Favorites int `json:"favorites"` Mid int `json:"mid"` Author string `json:"author"` Description string `json:"description"` Created string `json:"created"` Pic string `json:"pic"` Comment int `json:"comment"` Length string `json:"length"` } type userInfoResponse struct { Mid int `json:"mid"` Name string `json:"name"` Sex string `json:"sex"` Rank int `json:"rank"` Face string `json:"face"` Coins float32 `json:"coins"` RegTime int `json:"regtime"` Birthday string `json:"birthday"` Place string `json:"place"` Description string `json:"description"` Attentions []int `json:"attentions"` FansNum int `json:"fans"` FriendNum int `json:"friend"` AttentionNum int `json:"attention"` Sign string `json:"sign"` } type UserVideoResponse struct { } type UserService struct { BaseService } func (u *UserService) GetUserInfo(mid int) (*userInfoResponse, error) { retBody, err := u.doRequest("http://api.bilibili.cn/userinfo", map[string]string{ "mid": strconv.Itoa(mid), }) if err != nil { return nil, err } var ret userInfoResponse json.Unmarshal(retBody, &ret) return &ret, nil } func (u *UserService) GetUserVideos(mid, page, pageSize int) (*userVideosResponse, error) { retBody, err := u.doRequest("http://space.bilibili.com/ajax/member/getSubmitVideos", map[string]string{ "mid": strconv.Itoa(mid), "page": strconv.Itoa(page), "pagesize": strconv.Itoa(pageSize), }) if err != nil { return nil, err } var ret struct { Data userVideosResponse `json:"data"` } json.Unmarshal(retBody, &ret) return &ret.Data, nil } ================================================ FILE: client/utils.go ================================================ package client import ( "crypto/md5" "encoding/hex" "github.com/valyala/fasthttp" "time" "sort" "strings" "fmt" "sync" "errors" ) func EncodeSign(params map[string]string, secret string) (string, string) { queryString := httpBuildQuery(params) return queryString, Md5(queryString + secret) } func Md5(formal string) string { h := md5.New() h.Write([]byte(formal)) return hex.EncodeToString(h.Sum(nil)) } const ( HTTP_TIMEOUT = 2 HTTP_BUFFER_SIZE = 2 * 1024 ) var ( bufPool = &sync.Pool{New: func() interface{} { return make([]byte, HTTP_BUFFER_SIZE) }} //transport = http.Transport{ // Dial: func(network, addr string) (net.Conn, error) { // deadline := time.Now().Add((HTTP_TIMEOUT + 2) * time.Second) // c, err := net.DialTimeout(network, addr, HTTP_TIMEOUT*time.Second) // if err != nil { // return nil, err // } // c.SetDeadline(deadline) // return c, nil // }, // DisableKeepAlives: true, //} ) type HttpClient struct { client *fasthttp.Client } func NewHttpClient() HttpClient { return HttpClient{ client: &fasthttp.Client{ReadTimeout: HTTP_TIMEOUT * time.Second, WriteTimeout: HTTP_TIMEOUT * time.Second}, } } //map to query string & sort by key func httpBuildQuery(params map[string]string) string { list := make([]string, 0, len(params)) buffer := make([]string, 0, len(params)) for key := range params { list = append(list, key) } sort.Strings(list) for _, key := range list { value := params[key] buffer = append(buffer, key) buffer = append(buffer, "=") buffer = append(buffer, value) buffer = append(buffer, "&") } buffer = buffer[:len(buffer) - 1] return strings.Join(buffer, "") } func (b *HttpClient) Get(url string) ([]byte, error) { buf, _ := bufPool.Get().([]byte) defer bufPool.Put(buf) code, body, err := b.client.Get(buf, url) if err != nil { return nil, err } if code != 200 { return nil, errors.New(fmt.Sprintf("server return code %d", code)) } return body, nil } ================================================ FILE: client/video.go ================================================ package client import ( "encoding/json" "strconv" ) type videoElement struct { Aid string `json:"aid"` Mid int `json:"mid"` Copyright string `json:"copyright"` TypeId int `json:"typeid"` TypeName string `json:"typename"` Title string `json:"title"` SubTitle string `json:"subtitle"` Play int `json:"play"` Review int `json:"review"` VideoReview int `json:"video_review"` Favorites int `json:"favorites"` Author string `json:"author"` Description string `json:"description"` Create string `json:"create"` Pic string `json:"pic"` Credit int `json:"credit"` Coins int `json:"coins"` Duration string `json:"duration"` Comment int `json:"comment"` BadGePay bool `json:"badgepay"` } type videoMidInfo struct { Page int `json:"page"` Type string `json:"type"` Part string `json:"part"` Cid int `json:"cid"` Vid int `json:"vid"` } type videoInfoResponse struct { Tid int `json:"tid"` TypeName string `json:"typename"` ArcType string `json:"arctype"` Play string `json:"play"` Review string `json:"review"` VideoReview string `json:"video_review"` Favorites string `json:"favorites"` Title string `json:"title"` Description string `json:"description"` Tag string `json:"tag"` Pic string `json:"pic"` Author string `json:"author"` Mid string `json:"mid"` AuthorFace string `json:"face"` Pages int `json:"pages"` CreatedAt string `json:"created_at"` Coins string `json:"coins"` PartList map[string]videoMidInfo `json:"list"` } type videoDurl struct { Length int `json:"length"` Size int `json:"size"` Url string `json:"url"` BackupUrl []string `json:"backup_url"` } type videoPathResponse struct { result string `json:"result"` Format string `json:"format"` TimeLength int `json:"timelength"` AcceptFormat string `json:"accept_format"` AcceptQuality []int `json:"accept_quality"` List []videoDurl `json:"durl"` } type VideoService struct { BaseService } func (v *VideoService) GetVideoInfo(aid int) (*videoInfoResponse, error) { retBody, err := v.doRequest("http://api.bilibili.com/view", map[string]string{ "batch": "1", "check_area": "1", "id": strconv.Itoa(aid), "platform": "ios", "type": "json", }) if err != nil { return nil, err } var ret videoInfoResponse json.Unmarshal(retBody, &ret) return &ret, nil } /** videoType: "flv" "hdmp4" "mp4" quality: 1,2,3 */ func (v *VideoService) GetVideoPartPath(cid int, quality int) (*videoPathResponse, error) { query, sign := EncodeSign(map[string]string{ "cid": strconv.Itoa(cid), "from": "miniplay", "player": "1", "otype": "json", "type": "mp4", "quality": strconv.Itoa(quality), "appkey": "f3bb208b3d081dc8", }, "1c15888dc316e05a15fdd0a02ed6584f") url := "http://interface.bilibili.com/playurl?&" + query + "&sign=" + sign retBody, err := v.Client.Get(url) var ret videoPathResponse json.Unmarshal(retBody, &ret) return &ret, err } ================================================ FILE: conf.example.json ================================================ { "debug": true, "appkey": "", "secret": "" } ================================================ FILE: conf.json ================================================ { "debug": false, "appkey": "4ebafd7c4951b366", "secret": "8cb98205e9b2ad3669aad0fce12a4c13", "private": false, "allow_host": "http://bilibilih5.club" } ================================================ FILE: deploy.sh ================================================ #!/bin/bash IMAGENAME="bilibili-go" TAGENAME="whiteblue/bilibili-go" sudo -l || ( echo "Error: scripts need run with 'sudo'" && exit -1 ) ( cid=`sudo docker ps -a |grep $IMAGENAME | awk '{printf $1" "}'` if [ "$cid" != "" ];then sudo docker rm -f $cid && echo "delete container $cid" fi ) ( iid=`sudo docker images | grep $IMAGENAME | awk '{printf $3" "}'` if [ "$iid" != "" ];then sudo docker rmi $iid && echo "delete image $iid" fi ) sudo docker build -t $TAGENAME . || ( echo "Build image failed....." && exit -1 ) echo "build success" sudo docker run -d --name $IMAGENAME -p 80:8080 $TAGENAME echo "run container success" exit 0 ================================================ FILE: docs/api_doc.md ================================================ ## 接口地址 URL : ```http://bilibili-service.daoapp.io``` 基于DaoCloud免费容器 ## 接口文档 * 数据格式: ```application/json``` * 请求方式: ```post/get``` #### 基本状态码(HTTP)约定: 500: 服务器错误(API返回异常) 404: 请求的资源不可得 200: 成功 400: 参数异常 #### 错误返回格式: { "code": "PARAM_ERROR", "message": "request valitdate error" } #### 分类(sort)约定: '1' => '动画', '3' => '音乐', '4' => '游戏', '5' => '娱乐', '11' => '电视剧' '13' => '番剧', '23' => '电影', '36' => '科技', '119' => '鬼畜', '129' => '舞蹈', ### 1. 首页内容获取 --- 取得主要分类下的前10个热门视频 * URL: ```/allrank``` * 请求方式: GET * 示例: * ```curl -X GET -H "Content-Type: application/json" -H "Cache-Control: no-cache" "http://bilibili-service.daoapp.io/allrank"``` * 参数: 无 成功返回: ``` [ { "sort_name": "动画", "videos": [ { "aid": "5697545", "mid": 28965086, "copyright": "Original", "typeid": 27, "typename": "综合", "title": "【妈的智障】笑到胃疼的动画片段(第七期)片尾洗澡&足控福利", "subtitle": "", "play": 1398924, "review": 3435, "video_review": 24134, "favorites": 40908, "author": "噗汪汪", "description": "看你们谁还敢说我短!嗯?不知道怎么抽奖的请看上期视频。番名按照顺序分别是:银魂、超元气三姐妹、男子高中生的日常、妄想学生会、潜行吧奈亚子、濑户的花嫁、我们大家的河合庄和悠哉日常大王,片尾是银魂OAD。谢谢支持!", "create": "2016-08-07 19:12", "pic": "http://i2.hdslb.com/bfs/archive/7b2aced4ba0e5924b12fcf5a0ad36d8c2728b73d.jpg_320x200.jpg", "credit": 0, "coins": 6264, "duration": "24:28", "comment": 24134, "badgepay": false }, ... ] ``` ### 2. 分类排行获取 --- 各分类下的排行 * URL: ```/sort/{tid}``` * ```tid```为分类id(例如“动画”=>13) * 请求方式: GET * 示例: * ```curl -X GET -H "Content-Type: application/json" -H "Cache-Control: no-cache" "http://bilibili-service.daoapp.io/sort/13"``` * 参数: * ```page```: 页码 * ```count```: 分页容量 * ```order```: 排序方式(new,hot) 成功返回: ``` { "name": "番剧", "list": { "0": { "aid": "5698105", "mid": 21453565, "copyright": "Copy", "typeid": 33, "typename": "连载动画", "title": "【4月】Re:从零开始的异世界生活 19", "subtitle": "", "play": 2493341, "review": 42346, "video_review": 191673, "favorites": 2157, "author": "TV-TOKYO", "description": "#19 白鲸攻略战", "create": "2016-08-07 19:46", "pic": "http://i1.hdslb.com/bfs/archive/a0656101763a68a4bcb3fe603496037c253e106d.jpg_320x200.jpg", "credit": 0, "coins": 15908, "duration": "24:35", "comment": 191673, "badgepay": false }, "1":{...}, ... } } ``` ### 3. 视频信息获取 --- 各分类下的排行 * URL: ```/view/{aid}``` * aid => av号 * 请求方式: GET * 示例: * ```curl -X GET -H "Content-Type: application/json" -H "Cache-Control: no-cache" "http://bilibili-service.daoapp.io/view/5698105"``` * 参数: 无 成功返回: ``` { "tid": 33, "typename": "连载动画", "arctype": "Copy", "play": "2493735", "review": "42400", "video_review": "191632", "favorites": "2159", "title": "【4月】Re:从零开始的异世界生活 19", "description": "#19 白鲸攻略战", "tag": "TV动画,BILIBILI正版,RE:从零开始的异世界生活,从零开始的异世界生活", "pic": "http://i0.hdslb.com/bfs/archive/a0656101763a68a4bcb3fe603496037c253e106d.jpg", "author": "TV-TOKYO", "mid": "21453565", "face": "http://i0.hdslb.com/bfs/face/69ef6861067d6ef637b7c73b77d71c3414996745.jpg", "pages": 1, "created_at": "2016-08-08 01:05", "coins": "15911", "list": { "0": { "page": 1, "type": "vupload", "part": "ReZERO_19", "cid": 9253164, "vid": 0 } } } ``` ### 4. 视频地址解析 --- mp4/flv视频源取得,(注意某些老视频没有mp4源) * URL: ```/video/{cid}``` * 请求方式: GET * 示例: * ```curl -X GET -H "Cache-Control: no-cache" "http://bilibili-service.daoapp.io/video/9253164?quality=2"``` * 参数: * ```quality```:清晰度(1~2,根据视频有不同) * ```type```: 格式(mp4/flv) 成功返回: ``` { "format": "hdmp4", "timelength": 1476160, "accept_format": "mp4,hdmp4", "accept_quality": [ 2, 1 ], "durl": [ { "length": 1476160, "size": 206950377, "url": "http://cn-tj1-cu.acgvideo.com/vg123/3/2e/9253164-1-hd.mp4?expires=1471021200&ssig=1NyYrtPpFZmm4zHVClIHzA&oi=2067479167&rate=0", "backup_url": [ "http://cn-sddz2-cu.acgvideo.com/vg1/3/6d/9253164-1-hd.mp4?expires=1471021200&ssig=actJTZUGft5yN6fSQGL9Kw&oi=2067479167&rate=0", "http://cn-sdjn-cu-v-01.acgvideo.com/vg6/e/d5/9253164-1-hd.mp4?expires=1471021200&ssig=7I1sNpH6szF5CtNP4bwfXA&oi=2067479167&rate=0" ] } ] } ``` ### 5. 番剧更新列表 --- 目前B站版权二次元新番 * URL: ```/bangumi``` * 请求方式: GET * 示例: * ```curl -X GET -H "Cache-Control: no-cache" "http://bilibili-service.daoapp.io/bangumi"``` * 参数: 无 成功返回: ``` { "0": [ { "area": "日本", "arealimit": 0, "attention": 471585, "bangumi_id": 1070, "bgmcount": "24", "brief": null, "cover": "http://i1.hdslb.com/u_user/e6835e74ce9d6f63bc44d4f42dfc82e4.jpg", "danmaku_count": 439806, "favorites": 471585, "is_finish": 0, "lastupdate": 1451152800, "lastupdate_at": "2015-12-27 02:00:00", "new": true, "play_count": 9646746, "pub_time": "", "season_id": 2760, "spid": 56749, "square_cover": "http://i0.hdslb.com/sp/1e/1e21c6a6e17f5419eb1e10fadc53e6eb.jpg", "title": "终结的炽天使 第二季", "url": "/bangumi/i/2760/", "weekday": 0 }, ... ], ... } ``` ### 6. 专题信息查看 --- 例如番剧专题 * URL: * ```/spinfo/{spid}``` * 请求方式: GET * 示例: * ```curl -X GET -H "Cache-Control: no-cache" "http://bilibili-service.daoapp.io/spinfo/56749"``` * 参数: 无 成功返回: ``` { "alias": "终わりのセラフ", "alias_spid": 41465, "attention": 367438, "bangumi_date": "2015-10-01", "count": 40, "cover": "http://i0.hdslb.com/sp/1e/1e21c6a6e17f5419eb1e10fadc53e6eb.jpg", "create_at": "2014-12-12 21:19", "description": "电视动画《终结的炽天使》改编自日本轻小说家镜贵也原作、漫画家山本大和作画的同名漫画。\r\n2014年8月28日,发表了《终结的炽天使》电视动画化的决定。\r\n2014年12月20日,在日本千叶县幕张展览馆开幕的“Jump Festa 2015”会场上,宣布电视动画《终结的炽天使》会被分割成两个季度播出。\r\n第1期的播送时间为2015年4月4日-6月20日。\r\n第2期则是同年的10月至12月。", "favourite": 172114, "isbangumi": 1, "isbangumi_end": 1, "lastupdate": 1450364026, "lastupdate_at": "2015-12-17 22:53", "pubdate": 1418390386, "season": [ { "default": false, "index_cover": "http://i2.hdslb.com/sp/5c/5c7dbad52d522b6a5cbc8fe383ed92fe.jpg", "last_episode": null, "season_id": 2052, "season_name": "第一季", "video_view": 826087 }, { "default": false, "index_cover": "http://i1.hdslb.com/sp/87/87d650e53a8d50302a369365063c45a4.jpg", "last_episode": null, "season_id": 2053, "season_name": "第二季", "video_view": 4314354 } ], "season_id": 2053, "spid": 56749, "title": "终结的炽天使 第二季", "video_view": 28723627, "view": 1350787 } ``` ### 7. 专题视频获取 --- 取得专题下的所有视频 * URL: ```/spvideos/{spid}``` * 请求方式: GET * 示例: * ```curl -X GET -H "Cache-Control: no-cache" "http://bilibili-service.daoapp.io/spvideos/56749?bangumi=0"``` * 参数: * ```bangumi```: 取得番剧视频:1,其他视频:0 成功返回: ``` { "code": 0, "count": 17, "list": [ { "aid": 2330598, "cid": 3638258, "click": 498347, "cover": "http://i2.hdslb.com/video/51/512fc7fce5bb04a42fe116eb5500af20.jpg", "from": "vupload", "page": 0, "title": "「终结的炽天使」OP ED专辑" }, { "aid": 2425245, "cid": 3796297, "click": 101788, "cover": "http://i0.hdslb.com/video/4c/4cf6be151a858c200e4aa5ac07c2ccd1.jpg", "from": "vupload", "page": 0, "title": "让我们的炽天使燃起来吧Answer is near【MAD】" }, ... ], "results": 17, "spid": 56749 } ``` ### 8. 全站搜索 --- * URL: ```/search``` * 请求方式: POST * 示例: * ```curl -X POST -H "Cache-Control: no-cache" -H "Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW" -F "content=Fate" "http://bilibili-service.daoapp.io/search"``` * 参数: * ```content```: 搜索内容 * ```page```: 页码 * ```count```: 分页大小 成功返回: ``` { "page": 1, "pagesize": 20, "pageinfo": { "bangumi": { "total": 11, "numResults": 11, "pages": 4 }, "movie": { "total": 2, "numResults": 2, "pages": 1 }, "pgc": { "total": 2, "numResults": 2, "pages": 1 }, "special": { "total": 27, "numResults": 24, "pages": 8 }, "topic": { "total": 10, "numResults": 10, "pages": 4 }, "tvplay": { "total": 0, "numResults": 0, "pages": 1 }, "upuser": { "total": 86, "numResults": 86, "pages": 29 }, "video": { "total": 24742, "numResults": 999, "pages": 50 } }, "result": { "video": [ { "aid": "4912937", "mid": 777536, "copyright": "", "typeid": 0, "typename": "综合", "title": "【灵魂配音】10分钟演完fate stay night UBW", "subtitle": "", "play": 1121120, "review": 6922, "video_review": 18656, "favorites": 32058, "author": "LexBurner", "description": "自制 试水作,感谢新月冰冰配以及小鹤儿的帮忙,希望以后参与的人能越来越多,做的越来越好玩,这次还不是很到位,以后继续努力啦\r\nlex的零食铺:http://lexzhils.taobao.com\r\nlex的新浪微博:http://weibo.com/lexburner\r\n新月冰冰视频空间:http://space.bilibili.com/3295/#!/index\r\n小鹤儿视频空间:http://space.bilibili.com/6719190/#!/index", "create": "", "pic": "http://i0.hdslb.com/bfs/archive/7edf866255ae2d9a8f31c176c0873769d6451243.jpg_320x200.jpg", "credit": 0, "coins": 0, "duration": "10:33", "comment": 0, "badgepay": false }, ... ] } } ``` ### 9. 用户信息 --- * URL: ```/user/{mid}``` * 请求方式: GET * 示例: * ```curl -X GET -H "Cache-Control: no-cache" "http://bilibili-service.daoapp.io/user/116683"``` * 参数: 无 成功返回: ``` { "mid": 116683, "name": "=咬人猫=", "sex": "女", "rank": 10000, "face": "http://i1.hdslb.com/bfs/face/8fad84a4470f3d894d8f0dc95555ab8f2cb10a83.jpg", "coins": 63211.8, "regtime": 1301718879, "birthday": "0000-00-00", "place": "", "description": "bilibili 知名舞见", "attentions": [ 179628, 271126, 622863, 5055, 433715, 6870383, 4350178, 8084905 ], "fans": 627933, "friend": 8, "attention": 8, "sign": "面瘫女仆酱~小粗腿~事业线什么的!!吐槽你就输了!喵~" } ``` ### 10. 用户视频 --- * URL: ```/uservideos/{mid}``` * 请求方式: GET * 示例: * ```curl -X GET -H "Cache-Control: no-cache" "http://bilibili-service.daoapp.io/uservideos/116683"``` * 参数: * ```page```: 页码 * ```count```: 分页容量 成功返回: ``` { "vlist": [ { "aid": 5682645, "copyright": "Original", "typeid": 154, "title": "【咬人猫】落花情❤ o(*≧▽≦)ツ", "subtitle": "", "play": 752435, "review": 6402, "video_review": 9549, "favorites": 25734, "mid": 116683, "author": "=咬人猫=", "description": "也许是有史以来最忐忑的一次投稿,这次尝试的风格对我来说挑战很大,完全没有这类舞蹈的基础,所以当时这支舞挖坑填了一段时间因为实在是能力有限,被我搁置了很久~后来因为确实是太喜欢这种感觉的舞蹈了,又重新鼓起劲去学,也尝试了不同的服装版本录制了很多次,非常想完成这一支舞,虽然还有很多地方不够好,也希望大家多多包涵,谢谢大家的支持和等待~\n服装:七秀萝莉定国套\n舞蹈歌曲:七朵组合(一代)的作品《落花情》。", "created": "2016-08-06 20:20:45", "pic": "http://i0.hdslb.com/bfs/archive/a96729ac9e0a14544d1fcdb1471d23cf7ac1e61b.jpg", "comment": 9549, "length": "03:46" }, ... ] } ``` ### 11. 新番推荐 --- * URL: ```/bangumiindex``` * 请求方式: GET * 示例: * ```curl -X GET -H "Cache-Control: no-cache" "http://bilibili-service.daoapp.io/bangumiindex"``` 参数: 无 成功返回: ``` { "banners": [ { "title": "美术社大有问题", "link": "http://www.bilibili.com/bangumi/i/5043/", "img": "http://i0.hdslb.com/bfs/archive/c69c196266bfe6fb52e05d5752f4687e501d83e8.jpg", "simg": "", "aid": 0, "type": "link", "platform": 0, "pid": 0 }, { "title": "魔法战争", "link": "http://www.bilibili.com/bangumi/i/4367/", "img": "http://i0.hdslb.com/bfs/archive/3324a9fc99275ef22d0364bd3221e009020d9567.jpg", "simg": "", "aid": 0, "type": "link", "platform": 0, "pid": 0 }, { "title": "月歌", "link": "http://bangumi.bilibili.com/anime/5038", "img": "http://i0.hdslb.com/bfs/archive/dfbc9098218c7deffed53dd9c14619d88ac8f180.jpg", "simg": "", "aid": 0, "type": "link", "platform": 0, "pid": 0 }, { "title": "灵能百分百", "link": "http://bangumi.bilibili.com/anime/5058", "img": "http://i0.hdslb.com/bfs/archive/35e322a660aa83ada2a6ae94923c27510f40fd26.jpg", "simg": "", "aid": 0, "type": "link", "platform": 0, "pid": 0 } ], "recommends": [ { "aid": "5753187", "title": "【蒼氏甜品坊】初恋怪兽「…这是我的广播、要怎么办呢?」", "subtitle": "", "play": 631, "review": 0, "video_review": 82, "favorites": 139, "mid": 628114, "author": "祈妹", "description": "TV动画初恋怪兽的应援广播,配信日为每周五,主持人为苍井翔太。\n一个与动画内容相比在污和hentai的层面毫不逊色的广播节目。\n这是甜品坊第一次做广播,由于人手、经验和水平的不足不能保证翻译完全准确,有错误的地方欢迎指正~", "create": "2016-08-10 22:24", "pic": "http://i0.hdslb.com/bfs/archive/8770dd5682edb0170b5d3bfe08b8763aff9f7e4d.jpg_320x200.jpg", "coins": 40, "duration": "128:36" }, ... ] } ``` ================================================ FILE: main.go ================================================ package main import ( "github.com/go-playground/log" "github.com/whiteblue/bilibili-go/service" ) func main() { app, err := service.NewApplication("conf.json") if err != nil { log.Fatal(err) } app.Router.Run(":8080") } ================================================ FILE: run.sh ================================================ #!/bin/sh godep go build && ./bilibili-go ================================================ FILE: service/application.go ================================================ package service import ( "github.com/gin-gonic/contrib/gzip" "github.com/gin-gonic/gin" "github.com/go-playground/log" "github.com/go-playground/log/handlers/console" "github.com/whiteblue/bilibili-go/client" "time" ) const ( INDEX_CACHE = "index" ALL_RANK_CACHE = "all_rank" BANGUMI_CACHE = "bangumi" BANGUMI_LIST_CACHE = "bangumi_list" SORT_TOP_CACHE = "sort-" LIVE_INDEX_CACHE = "live_index" INDEX_BANNER_CACHE = "index_banner" ) var ( ProdLevels = []log.Level{ log.InfoLevel, log.NoticeLevel, log.WarnLevel, log.ErrorLevel, log.PanicLevel, log.AlertLevel, log.FatalLevel, } ) type BiliBiliApplication struct { Router *gin.Engine Corn *CornService Conf *Config Client *client.BCli Cache *CacheManager } func NewApplication(configFile string) (*BiliBiliApplication, error) { conf, err := ReadConfigFromFile(configFile) if err != nil { return nil, err } cLog := console.New() if conf.Debug { log.RegisterHandler(cLog, log.AllLevels...) gin.SetMode(gin.DebugMode) } else { log.RegisterHandler(cLog, ProdLevels...) gin.SetMode(gin.ReleaseMode) } log.Info("conform config file") r := gin.New() //use gzip r.Use(gin.Recovery()) r.Use(gzip.Gzip(gzip.BestCompression)) //corn service corn := NewCornService() //bilibili client cli := client.NewClient(conf.Appkey, conf.Secret) cache := NewCacheManager() app := &BiliBiliApplication{Router: r, Corn: corn, Conf: conf, Client: cli, Cache: cache} ConformRoute(app) log.Info("conform route") conformTask(app) corn.Start() log.Info("conform task") log.Info("init complete, start listen...") return app, nil } func conformTask(app *BiliBiliApplication) { app.Corn.RegisterTask(&IndexInfoTask{CornTask: CornTask{Name: "index_info", Duration: 2 * time.Hour}, app: app}) app.Corn.RegisterTask(&BangumiInfoTask{CornTask: CornTask{Name: "bangumi_info", Duration: 6 * time.Hour}, app: app}) app.Corn.RegisterTask(&BangumiListTask{CornTask: CornTask{Name: "bangumi_list", Duration: 6 * time.Hour}, app: app}) app.Corn.RegisterTask(&TopRankTask{CornTask: CornTask{Name: "top_rank", Duration: 2 * time.Hour}, app: app}) app.Corn.RegisterTask(&LiveIndexTask{CornTask: CornTask{Name: "alive_index", Duration: 2 * time.Hour}, app: app}) app.Corn.RegisterTask(&BannerTask{CornTask: CornTask{Name: "index_banner", Duration: 6 * time.Hour}, app: app}) } ================================================ FILE: service/cache.go ================================================ package service import "sync" type CacheManager struct { cacheMap map[string]interface{} lock *sync.RWMutex } func (c *CacheManager) GetCache(key string) interface{} { c.lock.RLock() defer c.lock.RUnlock() elem, ok := c.cacheMap[key] if !ok { return nil } return elem } func (c *CacheManager) SetCache(key string, value interface{}) { c.lock.Lock() defer c.lock.Unlock() c.cacheMap[key] = value } func NewCacheManager() *CacheManager { return &CacheManager{cacheMap: make(map[string]interface{}), lock: &sync.RWMutex{}} } ================================================ FILE: service/config.go ================================================ package service import ( "encoding/json" "io/ioutil" "os" ) type Config struct { Debug bool `json:"debug"` Appkey string `json:"appkey"` Secret string `json:"secret"` AllowHost string `json:"allow_host"` IsPrivate bool `json:"private"` } func ReadConfigFromFile(filename string) (*Config, error) { if _, err := os.Stat(filename); os.IsNotExist(err) { return nil, err } bytes, err := ioutil.ReadFile(filename) if err != nil { return nil, err } var conf Config err = json.Unmarshal(bytes, &conf) if err != nil { return nil, err } return &conf, nil } ================================================ FILE: service/corn.go ================================================ package service import ( "github.com/anacrolix/sync" "github.com/go-playground/log" "time" ) type CornTaskImpl interface { Run() error Success() Failure(error) GetName() string GetDuration() time.Duration GetLastRun() time.Time SyncLastRunTime() } type CornTask struct { Name string Duration time.Duration LastRun time.Time } func (t *CornTask) Run() error { return nil } func (t *CornTask) Success() { } func (t *CornTask) Failure(err error) { } func (t *CornTask) GetName() string { return t.Name } func (t *CornTask) GetDuration() time.Duration { return t.Duration } func (t *CornTask) GetLastRun() time.Time { return t.LastRun } func (t *CornTask) SyncLastRunTime() { t.LastRun = time.Now() } //execute task func exec(f CornTaskImpl) { log.Info("invoke task, taskname: ", f.GetName()) defer func() { if r := recover(); r != nil { log.Error(r) } }() if err := f.Run(); err != nil { f.Failure(err) } else { f.Success() } log.Info("run task end, taskname: ", f.GetName()) } type CornService struct { ticker *time.Ticker tasks []CornTaskImpl lock sync.Mutex done chan struct{} } func (c *CornService) RegisterTask(task CornTaskImpl) { task.SyncLastRunTime() exec(task) c.tasks = append(c.tasks, task) } func (c *CornService) syncTaskList(nowTime time.Time) { for _, task := range c.tasks { //Unix timestamp => duration between := time.Duration(nowTime.Unix()-task.GetLastRun().Unix()) * time.Second if between >= task.GetDuration() { task.SyncLastRunTime() exec(task) } } } func (c *CornService) loop() { for { select { case <-c.done: log.Info("corn loop stopped....") return case nowTime := <-c.ticker.C: go c.syncTaskList(nowTime) } } } func (c *CornService) Start() { go c.loop() } func (c *CornService) Stop() { c.ticker.Stop() close(c.done) } func NewCornService() *CornService { return &CornService{ ticker: time.NewTicker(time.Minute), tasks: []CornTaskImpl{}, lock: sync.Mutex{}, done: make(chan struct{}, 1), } } ================================================ FILE: service/corn_tasks.go ================================================ package service import ( "strconv" ) var ( _INDEX_SORTS = []int{ 24, 33, 31, 20, 17, 36, 119, } ) type SortRankInfo struct { SortId int `json:"sort_id"` Videos []interface{} `json:"videos"` } type IndexInfoTask struct { CornTask app *BiliBiliApplication } func (i *IndexInfoTask) Run() error { retInfo := make([]SortRankInfo, 0, len(_INDEX_SORTS)) for _, sortId := range _INDEX_SORTS { back, err := i.app.Client.Rank.SortRank(sortId, 1, 10, "view") if err != nil { return err } videos := make([]interface{}, 0, len(back)) for _, v := range (back) { videos = append(videos, v) } sortRank := SortRankInfo{SortId: sortId, Videos: videos} retInfo = append(retInfo, sortRank) sortCacheName := SORT_TOP_CACHE + strconv.Itoa(sortId) i.app.Cache.SetCache(sortCacheName, sortRank) } i.app.Cache.SetCache(INDEX_CACHE, retInfo) return nil } type BangumiInfoTask struct { CornTask app *BiliBiliApplication } func (i *BangumiInfoTask) Run() error { ret, err := i.app.Client.Bangumi.GetIndex() if err != nil { return err } i.app.Cache.SetCache(BANGUMI_CACHE, ret) return nil } type BangumiListTask struct { CornTask app *BiliBiliApplication } func (i *BangumiListTask) Run() error { ret, err := i.app.Client.Bangumi.GetWeekList("2") if err != nil { return err } i.app.Cache.SetCache(BANGUMI_LIST_CACHE, ret) return nil } type TopRankTask struct { CornTask app *BiliBiliApplication } func (i *TopRankTask) Run() error { ret, err := i.app.Client.Rank.SortRank(0, 1, 8, "hot") if err != nil { return err } i.app.Cache.SetCache(ALL_RANK_CACHE, ret) return nil } type LiveIndexTask struct { CornTask app *BiliBiliApplication } func (i *LiveIndexTask) Run() error { ret, err := i.app.Client.Others.AppIndex() if err != nil { return err } i.app.Cache.SetCache(LIVE_INDEX_CACHE, ret) return nil } type BannerTask struct { CornTask app *BiliBiliApplication } func (b *BannerTask) Run() error { ret, err := b.app.Client.Others.IndexBanner() if err != nil { return err } b.app.Cache.SetCache(INDEX_BANNER_CACHE, ret) return nil } ================================================ FILE: service/router.go ================================================ package service import ( "github.com/gin-gonic/gin" "strconv" "strings" ) func MakeFailedJsonMap(code string, message string) map[string]string { return map[string]string{ "code": code, "message": message, } } func ConformRoute(app *BiliBiliApplication) { allowOrigin := "*" if app.Conf.IsPrivate { allowOrigin = app.Conf.AllowHost } app.Router.Use(func(c *gin.Context) { c.Header("Access-Control-Allow-Origin", allowOrigin) c.Header("Access-Control-Allow-Headers", "Content-Type") c.Header("Access-Control-Max-Age", "7200") }) app.Router.GET("/", func(c *gin.Context) { c.JSON(200, gin.H{"message": "BiliBili-Service 2.1"}) }) app.Router.GET("/allrank", func(c *gin.Context) { back := app.Cache.GetCache(INDEX_CACHE) c.JSON(200, back) }) app.Router.GET("/toprank", func(c *gin.Context) { back := app.Cache.GetCache(ALL_RANK_CACHE) c.JSON(200, back) }) app.Router.GET("/view/:aid", func(c *gin.Context) { aid := c.Param("aid") aidNum, err := strconv.Atoi(aid) if err != nil { c.JSON(400, MakeFailedJsonMap("PARAM ERROR", err.Error())) return } list, err := app.Client.Video.GetVideoInfo(aidNum) if err != nil { c.JSON(404, MakeFailedJsonMap("VIDEO_NOT_FOUND", err.Error())) return } c.JSON(200, list) }) app.Router.GET("/video/:cid", func(c *gin.Context) { cid := c.Param("cid") quality := c.DefaultQuery("quality", "1") cidNum, err := strconv.Atoi(cid) if err != nil { c.JSON(400, MakeFailedJsonMap("PARAM ERROR", err.Error())) return } qualityNum, err := strconv.Atoi(quality) if err != nil { qualityNum = 1 } list, err := app.Client.Video.GetVideoPartPath(cidNum, qualityNum) if err != nil { c.JSON(404, MakeFailedJsonMap("VIDEO_NOT_FOUND", err.Error())) return } c.JSON(200, list) }) app.Router.GET("/user/:mid", func(c *gin.Context) { mid := c.Param("mid") midNum, err := strconv.Atoi(mid) if err != nil { c.JSON(400, MakeFailedJsonMap("PARAM ERROR", err.Error())) return } list, err := app.Client.User.GetUserInfo(midNum) if err != nil { c.JSON(404, MakeFailedJsonMap("USER_NOT_FOUND", err.Error())) return } c.JSON(200, list) }) app.Router.GET("/uservideos/:mid", func(c *gin.Context) { mid := c.Param("mid") page := c.DefaultQuery("page", "1") pageSize := c.DefaultQuery("page_size", "20") midNum, err := strconv.Atoi(mid) pageNum, err := strconv.Atoi(page) pageSizeNum, err := strconv.Atoi(pageSize) if err != nil { c.JSON(400, MakeFailedJsonMap("PARAM ERROR", err.Error())) return } list, err := app.Client.User.GetUserVideos(midNum, pageNum, pageSizeNum) if err != nil { c.JSON(404, MakeFailedJsonMap("USER_NOT_FOUND", err.Error())) return } c.JSON(200, list) }) app.Router.GET("/search", func(c *gin.Context) { content := c.Query("content") page := c.DefaultQuery("page", "1") pageSize := c.DefaultQuery("page_size", "20") order := c.DefaultQuery("order", "totalrank") var err error pageNum, err := strconv.Atoi(page) pageSizeNum, err := strconv.Atoi(pageSize) if err != nil { c.JSON(400, MakeFailedJsonMap("PARAM_ERROR", "")) return } if strings.TrimSpace(content) == "" { c.JSON(400, MakeFailedJsonMap("PARAM 'content' is '' or not set", "")) return } list, err := app.Client.Others.Search(content, pageNum, pageSizeNum, order) if err != nil { c.JSON(500, MakeFailedJsonMap("API_RETURN_ERROR", err.Error())) return } c.JSON(200, list) }) //type : // bangumi => 1 // user =>2 // movie=>3 // sp=>4 app.Router.GET("/searchbytype", func(c *gin.Context) { content := c.Query("content") page := c.DefaultQuery("page", "1") pageSize := c.DefaultQuery("page_size", "20") searchType := c.Query("type") var err error pageNum, err := strconv.Atoi(page) pageSizeNum, err := strconv.Atoi(pageSize) if err != nil { c.JSON(400, MakeFailedJsonMap("PARAM_ERROR", "")) return } if strings.TrimSpace(content) == "" { c.JSON(400, MakeFailedJsonMap("PARAM 'content' is '' or not set", "")) return } if strings.TrimSpace(searchType) == "" { c.JSON(400, MakeFailedJsonMap("PARAM 'type' is '' or not set", "")) return } typeInt := 1 switch searchType { case "user":typeInt = 2 case "movie":typeInt = 3 case "sp":typeInt = 4 } list, err := app.Client.Others.SearchByType(content, pageNum, pageSizeNum, typeInt) if err != nil { c.JSON(500, MakeFailedJsonMap("API_RETURN_ERROR", err.Error())) return } c.JSON(200, list) }) app.Router.GET("/top/:tid", func(c *gin.Context) { tid := c.Param("tid") var err error tidNum, err := strconv.Atoi(tid) if err != nil { c.JSON(400, MakeFailedJsonMap("PARAM_ERROR", err.Error())) return } cacheName := SORT_TOP_CACHE + strconv.Itoa(tidNum) target := app.Cache.GetCache(cacheName) if target == nil { c.JSON(404, MakeFailedJsonMap("SORT_NOT_FOUND", "")) return } c.JSON(200, target) }) app.Router.GET("/sort/:tid", func(c *gin.Context) { page := c.DefaultQuery("page", "1") pageSize := c.DefaultQuery("count", "20") tid := c.Param("tid") order := c.DefaultQuery("order", "hot") var err error tidNum, err := strconv.Atoi(tid) pageNum, err := strconv.Atoi(page) pageSizeNum, err := strconv.Atoi(pageSize) if err != nil { c.JSON(400, MakeFailedJsonMap("PARAM_ERROR", err.Error())) return } list, err := app.Client.Rank.SortRank(tidNum, pageNum, pageSizeNum, order) if err != nil { c.JSON(500, MakeFailedJsonMap("API_RETURN_ERROR", err.Error())) return } c.JSON(200, list) }) app.Router.GET("/spinfo/:spid", func(c *gin.Context) { spid := c.Param("spid") spidNum, err := strconv.Atoi(spid) if err != nil { c.JSON(400, MakeFailedJsonMap("PARAM_ERROR", "")) } list, err := app.Client.Special.GetSpecialInfo(spidNum) if err != nil { c.JSON(500, MakeFailedJsonMap("API_RETURN_ERROR", err.Error())) return } c.JSON(200, list) }) app.Router.GET("/bangumi", func(c *gin.Context) { back := app.Cache.GetCache(BANGUMI_LIST_CACHE) c.JSON(200, back) }) app.Router.GET("/bangumiinfo/:seasonid", func(c *gin.Context) { seasonId := c.Param("seasonid") back, err := app.Client.Bangumi.GetBangumiInfo(seasonId) if err != nil { c.JSON(500, MakeFailedJsonMap("API_RETURN_ERROR", err.Error())) return } c.JSON(200, back) }) app.Router.GET("/bangumiindex", func(c *gin.Context) { back := app.Cache.GetCache(BANGUMI_CACHE) c.JSON(200, back) }) app.Router.GET("/liveindex", func(c *gin.Context) { back := app.Cache.GetCache(LIVE_INDEX_CACHE) c.JSON(200, back) }) app.Router.GET("/banner", func(c *gin.Context) { back := app.Cache.GetCache(INDEX_BANNER_CACHE) c.JSON(200, back) }) } ================================================ FILE: test/api_test.go ================================================ package test import ( "encoding/json" "github.com/whiteblue/bilibili-go/client" "os" "strconv" "testing" ) const ( APPKEY = "4ebafd7c4951b366" SECRET = "8cb98205e9b2ad3669aad0fce12a4c13" ) func TestApiSortRank(t *testing.T) { c := client.NewClient(APPKEY, SECRET) back, err := c.Rank.SortRank(1, 1, 10, "hot") if err != nil { t.Error(err.Error()) t.Fail() } else { length := len(back.List) if length == 0 { t.Error("return length is 0") } for i := 0; i < length; i++ { index := strconv.Itoa(i) if back.List[index].Title == "" { t.Error("api return nil") } t.Log(back.List[index]) } } } func TestWeekBangumi(t *testing.T) { c := client.NewClient(APPKEY, SECRET) back, err := c.Bangumi.GetWeekList("2") if err != nil { t.Error(err.Error()) t.Failed() } else { if len(back.List) == 0 { t.Error("return length is 0") } for _, element := range back.List { if element.Title == "" { t.Error("api return nil") } t.Log(element) } } } func TestBangumiIndex(t *testing.T) { c := client.NewClient(APPKEY, SECRET) back, err := c.Bangumi.GetIndex() if err != nil { t.Error(err.Error()) t.Failed() } else { if len(back.Banners) == 0 { t.Error("return banner length is 0") } for _, banner := range back.Banners { if banner.Title == "" { t.Error("api return nil") } t.Log(banner.Title) } if len(back.Recommends) == 0 { t.Error("return recommends length is 0") } for _, ele := range back.Recommends { if ele.Title == "" { t.Error("api return nil") } t.Log(ele) } f, err := os.Create("test.json") if err != nil { t.Error(err) } defer f.Close() jsonByte, _ := json.Marshal(back) f.WriteString(string(jsonByte)) } } func TestVideoInfo(t *testing.T) { c := client.NewClient(APPKEY, SECRET) back, err := c.Video.GetVideoInfo(5495647) if err != nil { t.Error(err.Error()) t.Failed() } else { if back.Title == "" { t.Error("return title is nil") } if len(back.PartList) == 0 { t.Error("return partlist length is 0") } t.Log(back) } } func TestVideoPath(t *testing.T) { c := client.NewClient(APPKEY, SECRET) back, err := c.Video.GetVideoPartPath(8932442, 1, "mp4") if err != nil { t.Error(err.Error()) t.Failed() } else { if len(back.List) == 0 { t.Error("return list length is 0") } for _, dUrl := range back.List { t.Log(dUrl) } } } func TestSearch(t *testing.T) { c := client.NewClient(APPKEY, SECRET) back, err := c.Others.Search("fate", 1, 10, "totalrank", "all") if err != nil { t.Error(err.Error()) t.Failed() } else { if len(back.PageInfo) == 0 { t.Error("return list length is 0") } for _, video := range back.Result.Videos { t.Log(video) } } } func TestSpInfo(t *testing.T) { c := client.NewClient(APPKEY, SECRET) back, err := c.Special.GetSpecialInfo(158) if err != nil { t.Error(err.Error()) t.Failed() } else { if back.Title == "" { t.Error("return title is nil") } t.Log(back) } } func TestSpVideos(t *testing.T) { c := client.NewClient(APPKEY, SECRET) back, err := c.Special.GetSpecialVideos(158, true) if err != nil { t.Error(err.Error()) t.Failed() } else { if len(back.List) == 0 { t.Error("return list is nil") } for _, ele := range back.List { t.Log(ele) } } } func TestUserInfo(t *testing.T) { c := client.NewClient(APPKEY, SECRET) back, err := c.User.GetUserInfo(591635) if err != nil { t.Error(err.Error()) t.Failed() } else { if back.Name == "" { t.Error("api return user name nil") } t.Log(back) } } func TestUserVideos(t *testing.T) { c := client.NewClient(APPKEY, SECRET) back, err := c.User.GetUserVideos(591635, 1, 10) if err != nil { t.Error(err.Error()) t.Failed() } else { if len(back.List) == 0 { t.Error("api return empty list") } if len(back.TypeIndex) == 0 { t.Error("type list is empty") } t.Log(back) } } func TestAppIndex(t *testing.T) { c := client.NewClient(APPKEY, SECRET) _, err := c.Others.AppIndex() if err != nil { t.Error(err) } if err != nil { t.Error(err) } }