├── .github ├── FUNDING.yml └── workflows │ └── release.yml ├── .gitignore ├── .goreleaser.yaml ├── LICENSE ├── README.md ├── base.go ├── bilibili.go ├── cmd └── olivetv │ └── main.go ├── douyin.go ├── douyin_test.go ├── example_test.go ├── go.mod ├── go.sum ├── huya.go ├── huya_test.go ├── inke.go ├── kuaishou.go ├── lang.go ├── model ├── bilibili.go ├── douyin.go ├── inke.go └── ks.go ├── site.go ├── streamlink.go ├── template.go ├── tiktok.go ├── tv.go ├── twitch.go ├── util ├── http.go └── util.go └── youtube.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: ['https://afdian.net/@luxcgo'] 2 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*' 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | goreleaser: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - 16 | name: Checkout 17 | uses: actions/checkout@v2 18 | with: 19 | fetch-depth: 0 20 | - 21 | name: Fetch all tags 22 | run: git fetch --force --tags 23 | - 24 | name: Set up Go 25 | uses: actions/setup-go@v2 26 | with: 27 | go-version: 1.18 28 | - 29 | name: Run GoReleaser 30 | uses: goreleaser/goreleaser-action@v2 31 | with: 32 | distribution: goreleaser 33 | version: latest 34 | args: release --rm-dist 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.TV_RELEASE_TOKEN }} 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | *.json -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | project_name: olivetv 2 | 3 | before: 4 | hooks: 5 | - go mod tidy 6 | 7 | builds: 8 | - binary: olivetv 9 | 10 | main: ./cmd/olivetv 11 | env: 12 | - CGO_ENABLED=0 13 | 14 | goos: 15 | - linux 16 | - darwin 17 | - windows 18 | - freebsd 19 | - openbsd 20 | goarch: 21 | - amd64 22 | - 386 23 | - arm 24 | - arm64 25 | - ppc64le 26 | - s390x 27 | goarm: 28 | - 7 29 | - 6 30 | - 5 31 | ignore: 32 | - goos: darwin 33 | goarch: 386 34 | - goos: openbsd 35 | goarch: arm 36 | - goos: openbsd 37 | goarch: arm64 38 | - goos: freebsd 39 | goarch: arm64 40 | 41 | changelog: 42 | skip: true 43 | 44 | archives: 45 | - id: olivetv 46 | name_template: '{{ .ProjectName }}_v{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' 47 | format: tar.gz 48 | format_overrides: 49 | - goos: windows 50 | format: zip 51 | files: 52 | - LICENSE 53 | - README.md 54 | 55 | checksum: 56 | name_template: "{{ .ProjectName }}_v{{ .Version }}_checksums.txt" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Olive 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OliveTV 2 | 3 | [![GoDoc](https://img.shields.io/badge/GoDoc-Reference-blue?style=for-the-badge&logo=go)](https://pkg.go.dev/github.com/go-olive/tv?tab=doc) 4 | [![GitHub Workflow Status](https://img.shields.io/github/workflow/status/go-olive/tv/goreleaser?style=for-the-badge)](https://github.com/go-olive/tv/actions/workflows/release.yml) 5 | [![Sourcegraph](https://img.shields.io/badge/view%20on-Sourcegraph-brightgreen.svg?style=for-the-badge&logo=sourcegraph)](https://sourcegraph.com/github.com/go-olive/tv) 6 | 7 | OliveTV is a CLI utility which gets stream url along with other streamer details. 8 | 9 | ## Installation 10 | 11 | * build from source 12 | 13 | `go install github.com/go-olive/tv/cmd/olivetv@latest` 14 | 15 | * download from [**releases**](https://github.com/go-olive/tv/releases) 16 | 17 | ## Quickstart 18 | 19 | After installing, simply use: 20 | 21 | ```sh 22 | olivetv -u https://www.huya.com/518512 23 | ``` 24 | 25 | or 26 | 27 | ```sh 28 | olivetv -sid huya -rid 518512 29 | ``` 30 | 31 | > Some platforms might need a cookie, use -c to set one. 32 | > 33 | > eg. `olivetv -u https://live.douyin.com/xxx -c cookie` 34 | 35 | | site | cookie example | 36 | | -------- | ------------------------------------------------------------ | 37 | | douyin | `"__ac_nonce=06245c89100e7ab2dd536; __ac_signature=_02B4Z6wo00f01LjBMSAAAIDBwA.aJ.c4z1C44TWAAEx696;"` | 38 | | kuaishou | `"did=web_d079abeeeba77349c6eb5724363b8958"` | 39 | 40 | ## API Guide 41 | 42 | This API is what powers the cli but is also available to developers that wish to make use of the data OliveTV can retrieve in their own application. 43 | 44 | ### Extracting streams 45 | 46 | ```go 47 | package main 48 | 49 | import ( 50 | "github.com/go-olive/tv" 51 | ) 52 | 53 | func main() { 54 | t, err := tv.New("huya", "518512") 55 | if err != nil { 56 | return 57 | } 58 | if err := t.Snap(); err != nil { 59 | return 60 | } 61 | if url, liveOn := t.StreamUrl(); liveOn { 62 | println("stream url: ", url) 63 | } 64 | } 65 | 66 | ``` 67 | 68 | ## Contributing 69 | 70 | All contributions are welcome. Feel free to open a new thread on the issue tracker or submit a new pull request. 71 | 72 | For developer, check out [template file](template.go) if you want to add a new site. 73 | 74 | ## Credits 75 | 76 | This project is inspired by [real-url](https://github.com/wbt5/real-url) and [streamlink](https://github.com/streamlink/streamlink). 77 | -------------------------------------------------------------------------------- /base.go: -------------------------------------------------------------------------------- 1 | package tv 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "strings" 7 | 8 | "golang.org/x/net/publicsuffix" 9 | ) 10 | 11 | type base struct{} 12 | 13 | func (b *base) Name() string { 14 | return "undefined" 15 | } 16 | 17 | func (b *base) Snap(tv *Tv) error { 18 | return fmt.Errorf("site(ID = %s) Snap Method not implemented", tv.SiteID) 19 | } 20 | 21 | func (b *base) Permit(roomUrl RoomUrl) (*Tv, error) { 22 | u, err := url.Parse(string(roomUrl)) 23 | if err != nil { 24 | return nil, err 25 | } 26 | eTLDPO, err := publicsuffix.EffectiveTLDPlusOne(u.Hostname()) 27 | if err != nil { 28 | return nil, err 29 | } 30 | siteID := strings.Split(eTLDPO, ".")[0] 31 | base := strings.TrimPrefix(u.Path, "/") 32 | roomIDTmp := strings.Split(base, "/") 33 | roomID := roomIDTmp[len(roomIDTmp)-1] 34 | return &Tv{ 35 | SiteID: siteID, 36 | RoomID: roomID, 37 | }, nil 38 | } 39 | -------------------------------------------------------------------------------- /bilibili.go: -------------------------------------------------------------------------------- 1 | package tv 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/go-olive/tv/model" 8 | "github.com/go-olive/tv/util" 9 | ) 10 | 11 | func init() { 12 | registerSite("bilibili", &bilibili{}) 13 | } 14 | 15 | type bilibili struct { 16 | base 17 | } 18 | 19 | func (this *bilibili) Name() string { 20 | return "哔哩哔哩" 21 | } 22 | 23 | func (this *bilibili) Snap(tv *Tv) error { 24 | tv.Info = &Info{ 25 | Timestamp: time.Now().Unix(), 26 | } 27 | 28 | options := []Option{ 29 | this.setRoomOn(), 30 | this.setStreamURL(), 31 | } 32 | 33 | for _, option := range options { 34 | if err := option(tv); err != nil { 35 | return err 36 | } 37 | } 38 | 39 | return nil 40 | } 41 | 42 | func (this *bilibili) setRoomOn() Option { 43 | type roomInit struct { 44 | Code int64 `json:"code"` 45 | Data struct { 46 | RoomID int64 `json:"room_id"` 47 | LiveStatus int64 `json:"live_status"` 48 | UID int64 `json:"uid"` 49 | } 50 | } 51 | 52 | return func(tv *Tv) error { 53 | roomInit := new(roomInit) 54 | req := &util.HttpRequest{ 55 | // https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/live/info.md#获取房间页初始化信息 56 | URL: "https://api.live.bilibili.com/room/v1/Room/room_init", 57 | Method: "POST", 58 | RequestData: map[string]interface{}{ 59 | "id": tv.RoomID, 60 | }, 61 | ResponseData: roomInit, 62 | ContentType: "application/form-data", 63 | } 64 | if err := req.Send(); err != nil { 65 | return err 66 | } 67 | if roomInit.Code != 0 || roomInit.Data.LiveStatus != 1 { 68 | return nil 69 | } 70 | 71 | tv.RoomID = fmt.Sprint(roomInit.Data.RoomID) 72 | tv.roomOn = true 73 | 74 | titleInfo := new(model.BilibiliRoomTitle) 75 | req = &util.HttpRequest{ 76 | URL: fmt.Sprintf("https://api.live.bilibili.com/xlive/web-room/v1/index/getInfoByRoom?room_id=%s", tv.RoomID), 77 | Method: "GET", 78 | ResponseData: titleInfo, 79 | ContentType: "application/json", 80 | } 81 | if err := req.Send(); err != nil { 82 | return nil 83 | } 84 | 85 | tv.roomName = titleInfo.Data.RoomInfo.Title 86 | return nil 87 | } 88 | } 89 | 90 | func (this *bilibili) setStreamURL() Option { 91 | return this.getRealURL 92 | } 93 | 94 | func (this *bilibili) getAutoGenerated(roomID string, currentQn int) (*model.BilibiliAutoGenerated, error) { 95 | auto := new(model.BilibiliAutoGenerated) 96 | req := &util.HttpRequest{ 97 | // https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/live/live_stream.md 98 | URL: "https://api.live.bilibili.com/xlive/web-room/v2/index/getRoomPlayInfo", 99 | Method: "GET", 100 | RequestData: map[string]interface{}{ 101 | "room_id": roomID, 102 | "protocol": "0,1", 103 | "format": "0,1,2", 104 | "codec": "0,1", 105 | "qn": currentQn, 106 | "platform": "h5", 107 | "ptype": "8", 108 | }, 109 | ResponseData: auto, 110 | ContentType: "application/form-data", 111 | } 112 | err := req.Send() 113 | return auto, err 114 | } 115 | 116 | func (this *bilibili) getRealURL(tv *Tv) error { 117 | if !tv.roomOn { 118 | return nil 119 | } 120 | 121 | // 原画画质 122 | const highestQn = 10000 123 | auto, err := this.getAutoGenerated(tv.RoomID, highestQn) 124 | if err != nil { 125 | return err 126 | } 127 | for _, stream := range auto.Data.PlayurlInfo.Playurl.Stream { 128 | if stream.ProtocolName != "http_stream" { 129 | continue 130 | } 131 | for _, format := range stream.Format { 132 | if format.FormatName != "flv" { 133 | continue 134 | } 135 | var qnMax int 136 | if len(format.Codec) <= 0 { 137 | continue 138 | } 139 | for _, qn := range format.Codec[0].AcceptQn { 140 | if qn > qnMax { 141 | qnMax = qn 142 | } 143 | } 144 | if format.Codec[0].CurrentQn != qnMax && qnMax > 0 { 145 | auto, err = this.getAutoGenerated(tv.RoomID, qnMax) 146 | if err != nil { 147 | return err 148 | } 149 | } 150 | } 151 | } 152 | 153 | for _, stream := range auto.Data.PlayurlInfo.Playurl.Stream { 154 | if stream.ProtocolName != "http_stream" { 155 | continue 156 | } 157 | for _, format := range stream.Format { 158 | if format.FormatName != "flv" { 159 | continue 160 | } 161 | if len(format.Codec) <= 0 { 162 | continue 163 | } 164 | baseURL := format.Codec[0].BaseURL 165 | urlInfo := format.Codec[0].URLInfo[0] 166 | streamURL := urlInfo.Host + baseURL + urlInfo.Extra 167 | tv.streamUrl = streamURL 168 | return nil 169 | } 170 | } 171 | if tv.streamUrl == "" { 172 | tv.roomOn = false 173 | } 174 | return fmt.Errorf("fail to getRealURL") 175 | } 176 | -------------------------------------------------------------------------------- /cmd/olivetv/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | 7 | "github.com/go-olive/tv" 8 | ) 9 | 10 | var ( 11 | cookie string 12 | url string 13 | roomID string 14 | siteID string 15 | ) 16 | 17 | func init() { 18 | flag.StringVar(&cookie, "c", "", "site cookie") 19 | flag.StringVar(&url, "u", "", "room url") 20 | flag.StringVar(&roomID, "rid", "", "room ID") 21 | flag.StringVar(&siteID, "sid", "", "site ID") 22 | flag.Parse() 23 | } 24 | 25 | func main() { 26 | switch { 27 | case url != "": 28 | t, err := tv.NewWithUrl(url, tv.SetCookie(cookie)) 29 | if err != nil { 30 | println(err.Error()) 31 | return 32 | } 33 | t.Snap() 34 | fmt.Println(t) 35 | 36 | case roomID != "" && siteID != "": 37 | t, err := tv.New(siteID, roomID, tv.SetCookie(cookie)) 38 | if err != nil { 39 | println(err.Error()) 40 | return 41 | } 42 | t.Snap() 43 | fmt.Println(t) 44 | 45 | default: 46 | fmt.Println("You need to specify [roomd id and site id] or [room url]\nType olive -h for more information.") 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /douyin.go: -------------------------------------------------------------------------------- 1 | package tv 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/url" 7 | "strings" 8 | "time" 9 | 10 | "github.com/go-olive/tv/model" 11 | "github.com/go-olive/tv/util" 12 | jsoniter "github.com/json-iterator/go" 13 | ) 14 | 15 | var ( 16 | ErrCookieNotSet = errors.New("cookie not configured") 17 | ) 18 | 19 | func init() { 20 | registerSite("douyin", &douyin{}) 21 | } 22 | 23 | type douyin struct { 24 | base 25 | } 26 | 27 | func (this *douyin) Name() string { 28 | return "抖音" 29 | } 30 | 31 | func (this *douyin) Snap(tv *Tv) error { 32 | tv.Info = &Info{ 33 | Timestamp: time.Now().Unix(), 34 | } 35 | return this.set(tv) 36 | } 37 | 38 | func (this *douyin) set(tv *Tv) error { 39 | if tv.cookie == "" { 40 | return ErrCookieNotSet 41 | } 42 | req := &util.HttpRequest{ 43 | URL: fmt.Sprintf("https://live.douyin.com/%s", tv.RoomID), 44 | Method: "GET", 45 | ResponseData: *new(string), 46 | ContentType: "application/json", 47 | Header: map[string]string{ 48 | "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36 Edg/94.0.992.38", 49 | "referer": "https://live.douyin.com/", 50 | "cookie": tv.cookie, 51 | }, 52 | } 53 | var err error 54 | if err = req.Send(); err != nil { 55 | return err 56 | } 57 | resp := fmt.Sprint(req.ResponseData) 58 | splits := strings.Split(resp, ``)[0] 64 | resp, err = url.QueryUnescape(resp) 65 | if err != nil { 66 | return err 67 | } 68 | var autoGenerated model.DouyinAutoGenerated 69 | err = jsoniter.UnmarshalFromString(resp, &autoGenerated) 70 | if err != nil { 71 | return err 72 | } 73 | 74 | // 抖音 status == 2 代表是开播的状态 75 | if autoGenerated.App.InitialState.RoomStore.RoomInfo.Room.Status != 2 { 76 | return nil 77 | } 78 | 79 | streamDataStr := autoGenerated.App.InitialState.RoomStore.RoomInfo.Room.StreamURL.LiveCoreSdkData.PullData.StreamData 80 | var streamData model.DouyinStreamData 81 | err = jsoniter.UnmarshalFromString(streamDataStr, &streamData) 82 | if err != nil { 83 | // log.Println(err.Error()) 84 | return nil 85 | } 86 | tv.streamUrl = streamData.Data.Origin.Main.Flv 87 | tv.roomOn = true 88 | 89 | tv.roomName = autoGenerated.App.InitialState.RoomStore.RoomInfo.Room.Title 90 | 91 | return nil 92 | } 93 | -------------------------------------------------------------------------------- /douyin_test.go: -------------------------------------------------------------------------------- 1 | package tv_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/go-olive/tv" 7 | ) 8 | 9 | func TestDouyin_Snap(t *testing.T) { 10 | u := "https://live.douyin.com/278246244716" 11 | cookie := "__ac_nonce=062f60f640020c76e5b7c; __ac_signature=_02B4Z6wo00f011BxI-gAAIDAxKd45w5q9dNQUSdAALb1WEJMf.7Ma1NuqG0oiO7cYko5mx60CrOZ7DEMeiImZWCkLuxGYUV7nL8NBVxgyWeCnQdWfhttJNV21omp7bThIi8SVu-58ihu3EiU32;" 12 | dy, err := tv.NewWithUrl(u, tv.SetCookie(cookie)) 13 | if err != nil { 14 | println(err.Error()) 15 | return 16 | } 17 | dy.Snap() 18 | t.Log(dy) 19 | } 20 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package tv_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/go-olive/tv" 8 | ) 9 | 10 | func ExampleTv() { 11 | t, err := tv.New("huya", "518512") 12 | if err != nil { 13 | println(err.Error()) 14 | return 15 | } 16 | 17 | t.Snap() 18 | fmt.Println(t) 19 | } 20 | 21 | func ExampleSetCookie() { 22 | douyinCookie := "__ac_nonce=06245c89100e7ab2dd536; __ac_signature=_02B4Z6wo00f01LjBMSAAAIDBwA.aJ.c4z1C44TWAAEx696;" 23 | t, err := tv.New("douyin", "600571451250", tv.SetCookie(douyinCookie)) 24 | if err != nil { 25 | println(err.Error()) 26 | return 27 | } 28 | 29 | t.Snap() 30 | fmt.Println(t) 31 | } 32 | 33 | func ExampleNewWithUrl() { 34 | t, err := tv.NewWithUrl("https://www.huya.com/518512") 35 | if err != nil { 36 | println(err.Error()) 37 | return 38 | } 39 | 40 | t.Snap() 41 | fmt.Println(t) 42 | } 43 | 44 | func TestExampleTv(t *testing.T) { 45 | if !testing.Verbose() { 46 | return 47 | } 48 | ExampleTv() 49 | } 50 | 51 | func TestExampleSetCookie(t *testing.T) { 52 | if !testing.Verbose() { 53 | return 54 | } 55 | ExampleSetCookie() 56 | } 57 | 58 | func TestExampleNewWithUrl(t *testing.T) { 59 | if !testing.Verbose() { 60 | return 61 | } 62 | ExampleNewWithUrl() 63 | } 64 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-olive/tv 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/Davincible/gotiktoklive v0.0.0-20220607111918-9d1ed1997665 7 | github.com/json-iterator/go v1.1.12 8 | golang.org/x/net v0.0.0-20220412020605-290c469a71a5 9 | ) 10 | 11 | require ( 12 | github.com/gobwas/httphead v0.1.0 // indirect 13 | github.com/gobwas/pool v0.2.1 // indirect 14 | github.com/gobwas/ws v1.1.0 // indirect 15 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect 16 | github.com/modern-go/reflect2 v1.0.2 // indirect 17 | github.com/pkg/errors v0.9.1 // indirect 18 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect 19 | google.golang.org/protobuf v1.27.1 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Davincible/gotiktoklive v0.0.0-20220525135242-dda499f27e6d h1:lgHiDk84ursJQbRjemDG2iWjkdi/bKAcw80Uh3tbRS4= 2 | github.com/Davincible/gotiktoklive v0.0.0-20220525135242-dda499f27e6d/go.mod h1:P+iD6ERv+HzsVHS4rOL3xN6MLm8Ts/2J6K/2kRDqSJY= 3 | github.com/Davincible/gotiktoklive v0.0.0-20220607111918-9d1ed1997665 h1:VXTUBTs4dTtlFzyhQk2I40U9p7Pufuz2dP60SBf+fEk= 4 | github.com/Davincible/gotiktoklive v0.0.0-20220607111918-9d1ed1997665/go.mod h1:4GsgAld3ZSYhfXfFyROe6Kff8dkkhooay8I7qQayBzs= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= 9 | github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= 10 | github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= 11 | github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= 12 | github.com/gobwas/ws v1.1.0 h1:7RFti/xnNkMJnrK7D1yQ/iCIB5OrrY/54/H930kIbHA= 13 | github.com/gobwas/ws v1.1.0/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0= 14 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 15 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 16 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 17 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 18 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 19 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 20 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 21 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 22 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 23 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 24 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 25 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 26 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 27 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 28 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 29 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 30 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 31 | golang.org/x/net v0.0.0-20220412020605-290c469a71a5 h1:bRb386wvrE+oBNdF1d/Xh9mQrfQ4ecYhW5qJ5GvTGT4= 32 | golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 33 | golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 34 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= 35 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 36 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 37 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 38 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 39 | google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= 40 | google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 41 | -------------------------------------------------------------------------------- /huya.go: -------------------------------------------------------------------------------- 1 | package tv 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "html" 7 | "net/url" 8 | "regexp" 9 | "strconv" 10 | "strings" 11 | "time" 12 | 13 | "github.com/go-olive/tv/util" 14 | ) 15 | 16 | func init() { 17 | registerSite("huya", &huya{}) 18 | } 19 | 20 | type huya struct { 21 | base 22 | } 23 | 24 | func (this *huya) Snap(tv *Tv) error { 25 | tv.Info = &Info{ 26 | Timestamp: time.Now().Unix(), 27 | } 28 | 29 | options := []Option{ 30 | this.setRoomOn(), 31 | this.setStreamURL(), 32 | } 33 | 34 | for _, option := range options { 35 | if err := option(tv); err != nil { 36 | return err 37 | } 38 | } 39 | 40 | return nil 41 | } 42 | 43 | func (this *huya) Name() string { 44 | return "虎牙" 45 | } 46 | 47 | func (this *huya) streamURL(roomID string) (string, error) { 48 | roomURL := fmt.Sprintf("https://m.huya.com/%s", roomID) 49 | userAgent := "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko); Chrome/75.0.3770.100 Mobile Safari/537.36 " 50 | req := &util.HttpRequest{ 51 | URL: roomURL, 52 | Method: "GET", 53 | ResponseData: *new(string), 54 | ContentType: "application/x-www-form-urlencoded", 55 | Header: map[string]string{ 56 | "User-Agent": userAgent, 57 | }, 58 | } 59 | if err := req.Send(); err != nil { 60 | return "", err 61 | } 62 | respBody := fmt.Sprint(req.ResponseData) 63 | re := regexp.MustCompile(`liveLineUrl":"([^"]+)",`) 64 | res := re.FindStringSubmatch(respBody) 65 | if len(res) > 0 { //有直播链接 66 | u := res[1] 67 | if len(u) > 0 { 68 | decodedRet, _ := base64.StdEncoding.DecodeString(u) 69 | decodedUrl := string(decodedRet) 70 | if strings.Contains(decodedUrl, "replay") { //重播 71 | return "https:" + u, nil 72 | } else { 73 | liveLineUrl := this.proc(decodedUrl) 74 | liveLineUrl = strings.Replace(liveLineUrl, "hls", "flv", -1) 75 | liveLineUrl = strings.Replace(liveLineUrl, "m3u8", "flv", -1) 76 | return "https:" + liveLineUrl, nil 77 | } 78 | } 79 | } 80 | 81 | return "", nil 82 | } 83 | 84 | func (this *huya) proc(e string) string { 85 | i := strings.Split(e, "?")[0] 86 | b := strings.Split(e, "?")[1] 87 | r := strings.Split(i, "/") 88 | re := regexp.MustCompile(".(flv|m3u8)") 89 | s := re.ReplaceAllString(r[len(r)-1], "") 90 | srcAntiCode := html.UnescapeString(b) 91 | 92 | c := strings.Split(srcAntiCode, "&") 93 | cc := c[:0] 94 | n := make(map[string]string) 95 | for _, x := range c { 96 | if len(x) > 0 { 97 | cc = append(cc, x) 98 | ss := strings.Split(x, "=") 99 | n[ss[0]] = ss[1] 100 | } 101 | } 102 | c = cc 103 | fm, _ := url.QueryUnescape(n["fm"]) 104 | uu, _ := base64.StdEncoding.DecodeString(fm) 105 | u := string(uu) 106 | p := strings.Split(u, "_")[0] 107 | f := strconv.FormatInt(time.Now().UnixNano()/100, 10) 108 | l := n["wsTime"] 109 | t := "0" 110 | h := p + "_" + t + "_" + s + "_" + f + "_" + l 111 | m := util.GetMd5Hash(h) 112 | url := fmt.Sprintf("%s?wsSecret=%s&wsTime=%s&u=%s&seqid=%s&txyp=%s&fs=%s&sphdcdn=%s&sphdDC=%s&sphd=%s&u=0&t=100&sv=", i, m, l, t, f, n["txyp"], n["fs"], n["sphdcdn"], n["sphdDC"], n["sphd"]) 113 | return url 114 | } 115 | 116 | func (this *huya) setRoomOn() Option { 117 | return func(tv *Tv) error { 118 | webUserAgent := "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36" 119 | roomURL := fmt.Sprintf("https://www.huya.com/%s", tv.RoomID) 120 | req := &util.HttpRequest{ 121 | URL: roomURL, 122 | Method: "GET", 123 | ResponseData: *new(string), 124 | ContentType: "application/x-www-form-urlencoded", 125 | Header: map[string]string{ 126 | "User-Agent": webUserAgent, 127 | }, 128 | } 129 | if err := req.Send(); err != nil { 130 | return err 131 | } 132 | resp := fmt.Sprint(req.ResponseData) 133 | tv.roomOn = strings.Contains(resp, `"isOn":true`) 134 | 135 | titleRe := regexp.MustCompile(`host-title" title="([^"]+)">`) 136 | titleSubmatch := titleRe.FindAllStringSubmatch(resp, -1) 137 | titleRes := make([]string, 0) 138 | for _, v := range titleSubmatch { 139 | titleRes = append(titleRes, string(v[1])) 140 | } 141 | if len(titleRes) > 0 { 142 | tv.roomName = titleRes[0] 143 | } 144 | 145 | return nil 146 | } 147 | } 148 | 149 | func (this *huya) setStreamURL() Option { 150 | return func(tv *Tv) (err error) { 151 | if !tv.roomOn { 152 | return nil 153 | } 154 | tv.streamUrl, err = this.streamURL(tv.RoomID) 155 | if !strings.Contains(tv.streamUrl, "https") { 156 | tv.roomOn = false 157 | } 158 | return 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /huya_test.go: -------------------------------------------------------------------------------- 1 | package tv_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/go-olive/tv" 7 | ) 8 | 9 | func TestHuya_Snap(t *testing.T) { 10 | u := "https://www.huya.com/520588" 11 | huya, err := tv.NewWithUrl(u) 12 | if err != nil { 13 | println(err.Error()) 14 | return 15 | } 16 | huya.Snap() 17 | t.Log(huya) 18 | } 19 | -------------------------------------------------------------------------------- /inke.go: -------------------------------------------------------------------------------- 1 | package tv 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/go-olive/tv/model" 7 | "github.com/go-olive/tv/util" 8 | ) 9 | 10 | func init() { 11 | registerSite("inke", &inke{}) 12 | } 13 | 14 | type inke struct { 15 | base 16 | } 17 | 18 | func (this *inke) Name() string { 19 | return "映客" 20 | } 21 | 22 | func (this *inke) Snap(tv *Tv) error { 23 | tv.Info = &Info{ 24 | Timestamp: time.Now().Unix(), 25 | } 26 | return this.set(tv) 27 | } 28 | 29 | func (this *inke) set(tv *Tv) error { 30 | a := new(model.InkeAutoGenerated) 31 | req := &util.HttpRequest{ 32 | URL: "https://webapi.busi.inke.cn/web/live_share_pc?uid=" + tv.RoomID, 33 | Method: "GET", 34 | ResponseData: a, 35 | ContentType: "application/json", 36 | Header: map[string]string{ 37 | "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36 Edg/94.0.992.38", 38 | }, 39 | } 40 | if err := req.Send(); err != nil { 41 | return err 42 | } 43 | tv.roomName = a.Data.LiveName 44 | tv.streamerName = a.Data.MediaInfo.Nick 45 | if len(a.Data.LiveAddr) > 0 { 46 | tv.streamUrl = a.Data.LiveAddr[0].StreamAddr 47 | tv.roomOn = true 48 | } 49 | 50 | return nil 51 | } 52 | -------------------------------------------------------------------------------- /kuaishou.go: -------------------------------------------------------------------------------- 1 | package tv 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "strconv" 7 | "strings" 8 | "time" 9 | 10 | "github.com/go-olive/tv/model" 11 | "github.com/go-olive/tv/util" 12 | ) 13 | 14 | const ( 15 | KsLiveDetailQuery = ` 16 | query LiveDetail($principalId: String) { 17 | liveDetail(principalId: $principalId) { 18 | liveStream 19 | } 20 | } 21 | ` 22 | KsUserInfoQuery = ` 23 | query userInfoQuery($principalId: String) { 24 | userInfo(principalId: $principalId) { 25 | name 26 | living 27 | } 28 | } 29 | ` 30 | ) 31 | 32 | func init() { 33 | registerSite("kuaishou", &kuaishou{}) 34 | } 35 | 36 | type kuaishou struct { 37 | base 38 | } 39 | 40 | func (this *kuaishou) Name() string { 41 | return "快手" 42 | } 43 | 44 | func (this *kuaishou) Snap(tv *Tv) (err error) { 45 | tv.Info = &Info{ 46 | Timestamp: time.Now().Unix(), 47 | } 48 | return this.setV2(tv) 49 | } 50 | 51 | func (this *kuaishou) setV1(tv *Tv) error { 52 | if err := this.setRoomOnV1(tv); err != nil { 53 | return err 54 | } 55 | return this.setStreamUrlV1(tv) 56 | } 57 | 58 | func (this *kuaishou) setStreamUrlV1(tv *Tv) error { 59 | if tv.cookie == "" { 60 | return ErrCookieNotSet 61 | } 62 | // if !tv.roomOn { 63 | // return nil 64 | // } 65 | ksAG := new(model.KsLiveDetailAutoGenerated) 66 | req := &util.HttpRequest{ 67 | URL: "https://live.kuaishou.com/graphql", 68 | Method: "POST", 69 | RequestData: map[string]interface{}{ 70 | "operationName": "LiveDetail", 71 | "variables": map[string]string{ 72 | "principalId": tv.RoomID, 73 | }, 74 | "query": KsLiveDetailQuery, 75 | }, 76 | ResponseData: ksAG, 77 | ContentType: "application/json", 78 | Header: map[string]string{ 79 | "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36 Edg/94.0.992.38", 80 | "referer": "https://live.kuaishou.com/", 81 | "cookie": tv.cookie, 82 | }, 83 | } 84 | if err := req.Send(); err != nil { 85 | return err 86 | } 87 | 88 | if len(ksAG.Data.WebLiveDetail.LiveStream.PlayUrls) > 0 { 89 | tv.roomOn = true 90 | tv.streamUrl = ksAG.Data.WebLiveDetail.LiveStream.PlayUrls[0].URL 91 | } 92 | tv.roomName = ksAG.Data.WebLiveDetail.LiveStream.Caption 93 | 94 | return nil 95 | } 96 | 97 | func (this *kuaishou) setRoomOnV1(tv *Tv) error { 98 | if tv.cookie == "" { 99 | return ErrCookieNotSet 100 | } 101 | ksAG := new(model.KsUserInfoAutoGenerated) 102 | req := &util.HttpRequest{ 103 | URL: "https://live.kuaishou.com/graphql", 104 | Method: "POST", 105 | RequestData: map[string]interface{}{ 106 | "operationName": "userInfoQuery", 107 | "variables": map[string]string{ 108 | "principalId": tv.RoomID, 109 | }, 110 | "query": KsUserInfoQuery, 111 | }, 112 | ResponseData: ksAG, 113 | ContentType: "application/json", 114 | Header: map[string]string{ 115 | "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36 Edg/94.0.992.38", 116 | "referer": "https://live.kuaishou.com/", 117 | "cookie": tv.cookie, 118 | }, 119 | } 120 | if err := req.Send(); err != nil { 121 | return err 122 | } 123 | 124 | tv.roomOn = ksAG.Data.UserInfo.Living 125 | tv.streamerName = ksAG.Data.UserInfo.Name 126 | 127 | return nil 128 | } 129 | 130 | func (this *kuaishou) setV2(tv *Tv) error { 131 | if tv.cookie == "" { 132 | return ErrCookieNotSet 133 | } 134 | req, err := http.NewRequest("GET", "https://live.kuaishou.com/profile/"+tv.RoomID, nil) 135 | if err != nil { 136 | return err 137 | } 138 | req.Header.Add("cookie", tv.cookie) 139 | resp, err := http.DefaultClient.Do(req) 140 | if err != nil { 141 | return err 142 | } 143 | body, err := io.ReadAll(resp.Body) 144 | if err != nil { 145 | return err 146 | } 147 | content := string(body) 148 | 149 | if !strings.Contains(content, "直播中") { 150 | return nil 151 | } 152 | 153 | tv.streamerName, _ = util.Match(`title="([^"]+)" target="_blank"`, content) 154 | tv.roomName, _ = util.Match(`title="([^"]+)" class="router-link-exact-active`, content) 155 | tv.streamUrl, _ = util.Match(`"url":"([^"]+)"`, content) 156 | tv.streamUrl, _ = strconv.Unquote(`"` + tv.streamUrl + `"`) 157 | if tv.streamUrl != "" { 158 | tv.roomOn = true 159 | } 160 | 161 | return nil 162 | } 163 | -------------------------------------------------------------------------------- /lang.go: -------------------------------------------------------------------------------- 1 | package tv 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | 8 | "github.com/go-olive/tv/util" 9 | ) 10 | 11 | func init() { 12 | registerSite("lang", &lang{}) 13 | } 14 | 15 | type lang struct { 16 | base 17 | } 18 | 19 | func (this *lang) Name() string { 20 | return "浪LIVE" 21 | } 22 | 23 | func (this *lang) Snap(tv *Tv) error { 24 | tv.Info = &Info{ 25 | Timestamp: time.Now().Unix(), 26 | } 27 | return this.set(tv) 28 | } 29 | 30 | func (this *lang) set(tv *Tv) (err error) { 31 | roomUrl := fmt.Sprintf("https://www.lang.live/room/%s", tv.RoomID) 32 | roomContent, err := util.GetURLContent(roomUrl) 33 | if err != nil { 34 | return err 35 | } 36 | roomContent = strings.ReplaceAll(roomContent, "\\", "") 37 | tv.streamUrl, err = util.Match(`"liveurl":"([^"]+)"`, roomContent) 38 | if err != nil { 39 | return err 40 | } 41 | title, _ := util.Match(`([^<]+)`, roomContent) 42 | tv.roomName = strings.Split(title, " - "+tv.RoomID)[0] 43 | if tv.streamerName == "" { 44 | tv.streamerName = tv.roomName 45 | } 46 | tv.roomOn = true 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /model/bilibili.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type BilibiliAutoGenerated struct { 4 | Code int `json:"code"` 5 | Message string `json:"message"` 6 | TTL int `json:"ttl"` 7 | Data struct { 8 | RoomID int `json:"room_id"` 9 | ShortID int `json:"short_id"` 10 | UID int `json:"uid"` 11 | IsHidden bool `json:"is_hidden"` 12 | IsLocked bool `json:"is_locked"` 13 | IsPortrait bool `json:"is_portrait"` 14 | LiveStatus int `json:"live_status"` 15 | HiddenTill int `json:"hidden_till"` 16 | LockTill int `json:"lock_till"` 17 | Encrypted bool `json:"encrypted"` 18 | PwdVerified bool `json:"pwd_verified"` 19 | LiveTime int `json:"live_time"` 20 | RoomShield int `json:"room_shield"` 21 | AllSpecialTypes []interface{} `json:"all_special_types"` 22 | PlayurlInfo struct { 23 | ConfJSON string `json:"conf_json"` 24 | Playurl struct { 25 | Cid int `json:"cid"` 26 | GQnDesc []struct { 27 | Qn int `json:"qn"` 28 | Desc string `json:"desc"` 29 | HdrDesc string `json:"hdr_desc"` 30 | } `json:"g_qn_desc"` 31 | Stream []struct { 32 | ProtocolName string `json:"protocol_name"` 33 | Format []struct { 34 | FormatName string `json:"format_name"` 35 | Codec []struct { 36 | CodecName string `json:"codec_name"` 37 | CurrentQn int `json:"current_qn"` 38 | AcceptQn []int `json:"accept_qn"` 39 | BaseURL string `json:"base_url"` 40 | URLInfo []struct { 41 | Host string `json:"host"` 42 | Extra string `json:"extra"` 43 | StreamTTL int `json:"stream_ttl"` 44 | } `json:"url_info"` 45 | HdrQn interface{} `json:"hdr_qn"` 46 | DolbyType int `json:"dolby_type"` 47 | } `json:"codec"` 48 | } `json:"format"` 49 | } `json:"stream"` 50 | P2PData struct { 51 | P2P bool `json:"p2p"` 52 | P2PType int `json:"p2p_type"` 53 | MP2P bool `json:"m_p2p"` 54 | MServers interface{} `json:"m_servers"` 55 | } `json:"p2p_data"` 56 | DolbyQn interface{} `json:"dolby_qn"` 57 | } `json:"playurl"` 58 | } `json:"playurl_info"` 59 | } `json:"data"` 60 | } 61 | 62 | type BilibiliRoomTitle struct { 63 | Data struct { 64 | RoomInfo struct { 65 | Title string `json:"title"` 66 | } `json:"room_info"` 67 | } `json:"data"` 68 | } 69 | -------------------------------------------------------------------------------- /model/douyin.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type DouyinAutoGenerated struct { 4 | App struct { 5 | InitialState struct { 6 | RoomStore struct { 7 | RoomInfo struct { 8 | Room struct { 9 | IDStr string `json:"id_str"` 10 | Status int `json:"status"` 11 | StatusStr string `json:"status_str"` 12 | Title string `json:"title"` 13 | UserCountStr string `json:"user_count_str"` 14 | Cover struct { 15 | URLList []string `json:"url_list"` 16 | } `json:"cover"` 17 | StreamURL struct { 18 | FlvPullURL struct { 19 | FullHd1 string `json:"FULL_HD1"` 20 | Hd1 string `json:"HD1"` 21 | Sd1 string `json:"SD1"` 22 | Sd2 string `json:"SD2"` 23 | } `json:"flv_pull_url"` 24 | DefaultResolution string `json:"default_resolution"` 25 | HlsPullURLMap struct { 26 | FullHd1 string `json:"FULL_HD1"` 27 | Hd1 string `json:"HD1"` 28 | Sd1 string `json:"SD1"` 29 | Sd2 string `json:"SD2"` 30 | } `json:"hls_pull_url_map"` 31 | HlsPullURL string `json:"hls_pull_url"` 32 | StreamOrientation int `json:"stream_orientation"` 33 | LiveCoreSdkData struct { 34 | PullData struct { 35 | Options struct { 36 | DefaultQuality struct { 37 | Name string `json:"name"` 38 | SdkKey string `json:"sdk_key"` 39 | VCodec string `json:"v_codec"` 40 | Resolution string `json:"resolution"` 41 | Level int `json:"level"` 42 | } `json:"default_quality"` 43 | Qualities []struct { 44 | Name string `json:"name"` 45 | SdkKey string `json:"sdk_key"` 46 | VCodec string `json:"v_codec"` 47 | Resolution string `json:"resolution"` 48 | Level int `json:"level"` 49 | } `json:"qualities"` 50 | } `json:"options"` 51 | StreamData string `json:"stream_data"` 52 | } `json:"pull_data"` 53 | } `json:"live_core_sdk_data"` 54 | } `json:"stream_url"` 55 | } `json:"room"` 56 | } `json:"roomInfo"` 57 | } `json:"roomStore"` 58 | } `json:"initialState"` 59 | } `json:"app"` 60 | } 61 | 62 | type DouyinStreamData struct { 63 | Common struct { 64 | SessionID string `json:"session_id"` 65 | RuleIds string `json:"rule_ids"` 66 | } `json:"common"` 67 | Data struct { 68 | Hd struct { 69 | Main struct { 70 | Flv string `json:"flv"` 71 | Hls string `json:"hls"` 72 | Cmaf string `json:"cmaf"` 73 | Dash string `json:"dash"` 74 | Lls string `json:"lls"` 75 | Tsl string `json:"tsl"` 76 | Tile string `json:"tile"` 77 | SdkParams string `json:"sdk_params"` 78 | } `json:"main"` 79 | } `json:"hd"` 80 | Sd struct { 81 | Main struct { 82 | Flv string `json:"flv"` 83 | Hls string `json:"hls"` 84 | Cmaf string `json:"cmaf"` 85 | Dash string `json:"dash"` 86 | Lls string `json:"lls"` 87 | Tsl string `json:"tsl"` 88 | Tile string `json:"tile"` 89 | SdkParams string `json:"sdk_params"` 90 | } `json:"main"` 91 | } `json:"sd"` 92 | Ld struct { 93 | Main struct { 94 | Flv string `json:"flv"` 95 | Hls string `json:"hls"` 96 | Cmaf string `json:"cmaf"` 97 | Dash string `json:"dash"` 98 | Lls string `json:"lls"` 99 | Tsl string `json:"tsl"` 100 | Tile string `json:"tile"` 101 | SdkParams string `json:"sdk_params"` 102 | } `json:"main"` 103 | } `json:"ld"` 104 | Origin struct { 105 | Main struct { 106 | Flv string `json:"flv"` 107 | Hls string `json:"hls"` 108 | Cmaf string `json:"cmaf"` 109 | Dash string `json:"dash"` 110 | Lls string `json:"lls"` 111 | Tsl string `json:"tsl"` 112 | Tile string `json:"tile"` 113 | SdkParams string `json:"sdk_params"` 114 | } `json:"main"` 115 | } `json:"origin"` 116 | } `json:"data"` 117 | } 118 | -------------------------------------------------------------------------------- /model/inke.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type InkeAutoGenerated struct { 4 | ErrorCode int `json:"error_code"` 5 | Message string `json:"message"` 6 | Data struct { 7 | MediaInfo struct { 8 | Nick string `json:"nick"` 9 | Portrait string `json:"portrait"` 10 | InkeID int `json:"inke_id"` 11 | Level int `json:"level"` 12 | Gender int `json:"gender"` 13 | GenderImg string `json:"gender_img"` 14 | Area string `json:"area"` 15 | Description string `json:"description"` 16 | LevelImg string `json:"level_img"` 17 | } `json:"media_info"` 18 | File struct { 19 | Status int `json:"status"` 20 | RecordURL string `json:"record_url"` 21 | OnlineUsers int `json:"online_users"` 22 | Title string `json:"title"` 23 | Pic string `json:"pic"` 24 | City string `json:"city"` 25 | } `json:"file"` 26 | Records []interface{} `json:"records"` 27 | Status int `json:"status"` 28 | Portrait string `json:"portrait"` 29 | Point int `json:"point"` 30 | SioIP string `json:"sio_ip"` 31 | Liveid string `json:"liveid"` 32 | LiveUID string `json:"live_uid"` 33 | IsFollow int `json:"is_follow"` 34 | WeiboURL string `json:"weibo_url"` 35 | WeixinURL string `json:"weixin_url"` 36 | Sec string `json:"sec"` 37 | ViewUID string `json:"view_uid"` 38 | Nonce string `json:"nonce"` 39 | Time int `json:"time"` 40 | SioMd5 string `json:"sio_md5"` 41 | ViewNick string `json:"view_nick"` 42 | Token string `json:"token"` 43 | TokenTime string `json:"token_time"` 44 | Origin interface{} `json:"origin"` 45 | IsLogin int `json:"is_login"` 46 | SioURL string `json:"sio_url"` 47 | LiveAddr []struct { 48 | Liveid string `json:"liveid"` 49 | StreamAddr string `json:"stream_addr"` 50 | HlsStreamAddr string `json:"hls_stream_addr"` 51 | RtmpStreamAddr string `json:"rtmp_stream_addr"` 52 | } `json:"live_addr"` 53 | LiveType string `json:"live_type"` 54 | SubLiveType string `json:"sub_live_type"` 55 | LiveName string `json:"live_name"` 56 | } `json:"data"` 57 | } 58 | -------------------------------------------------------------------------------- /model/ks.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type KsLiveDetailAutoGenerated struct { 4 | Data struct { 5 | WebLiveDetail struct { 6 | LiveStream struct { 7 | Caption string `json:"caption"` 8 | PlayUrls []struct { 9 | Quality string `json:"quality"` 10 | URL string `json:"url"` 11 | } `json:"playUrls"` 12 | } `json:"liveStream"` 13 | } `json:"webLiveDetail"` 14 | } `json:"data"` 15 | } 16 | 17 | type KsUserInfoAutoGenerated struct { 18 | Data struct { 19 | UserInfo struct { 20 | Name string `json:"name"` 21 | Living bool `json:"living"` 22 | } `json:"userInfo"` 23 | } `json:"data"` 24 | } 25 | -------------------------------------------------------------------------------- /site.go: -------------------------------------------------------------------------------- 1 | package tv 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | // map[string]site 8 | var sites sync.Map 9 | 10 | type Site interface { 11 | Name() string 12 | Snap(*Tv) error 13 | Permit(RoomUrl) (*Tv, error) 14 | } 15 | 16 | func registerSite(siteID string, site Site) { 17 | if _, dup := sites.LoadOrStore(siteID, site); dup { 18 | panic("site already registered") 19 | } 20 | } 21 | 22 | func Sniff(siteID string) (Site, bool) { 23 | s, ok := sites.Load(siteID) 24 | if !ok { 25 | return nil, ok 26 | } 27 | return s.(Site), ok 28 | } 29 | -------------------------------------------------------------------------------- /streamlink.go: -------------------------------------------------------------------------------- 1 | package tv 2 | 3 | import ( 4 | "os/exec" 5 | "time" 6 | ) 7 | 8 | func init() { 9 | registerSite("streamlink", &streamlink{}) 10 | } 11 | 12 | type streamlink struct { 13 | base 14 | } 15 | 16 | func (this *streamlink) Name() string { 17 | return "streamlink" 18 | } 19 | 20 | func (this *streamlink) Snap(tv *Tv) error { 21 | tv.Info = &Info{ 22 | Timestamp: time.Now().Unix(), 23 | } 24 | return this.set(tv) 25 | } 26 | 27 | func (this *streamlink) set(tv *Tv) error { 28 | cmd := exec.Command( 29 | "streamlink", 30 | tv.RoomID, 31 | ) 32 | if err := cmd.Run(); err != nil { 33 | return nil 34 | } 35 | 36 | tv.roomOn = true 37 | tv.streamUrl = tv.RoomID 38 | 39 | return nil 40 | } 41 | -------------------------------------------------------------------------------- /template.go: -------------------------------------------------------------------------------- 1 | package tv 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | func init() { 8 | registerSite("tmpl", &tmpl{}) 9 | } 10 | 11 | type tmpl struct { 12 | base 13 | } 14 | 15 | func (this *tmpl) Name() string { 16 | return "tmpl" 17 | } 18 | 19 | func (this *tmpl) Snap(tv *Tv) error { 20 | tv.Info = &Info{ 21 | Timestamp: time.Now().Unix(), 22 | } 23 | return this.set(tv) 24 | } 25 | 26 | func (this *tmpl) set(tv *Tv) error { 27 | tv.roomName = "tmpl room name" 28 | tv.streamerName = "tmpl streamer name" 29 | tv.roomOn = true 30 | tv.streamUrl = "tmpl stream url" 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /tiktok.go: -------------------------------------------------------------------------------- 1 | package tv 2 | 3 | import ( 4 | "log" 5 | "strings" 6 | "time" 7 | 8 | "github.com/Davincible/gotiktoklive" 9 | ) 10 | 11 | func init() { 12 | registerSite("tiktok", &tiktok{}) 13 | } 14 | 15 | type tiktok struct { 16 | base 17 | } 18 | 19 | func (this *tiktok) Name() string { 20 | return "tiktok" 21 | } 22 | 23 | func (this *tiktok) Snap(tv *Tv) error { 24 | tv.Info = &Info{ 25 | Timestamp: time.Now().Unix(), 26 | } 27 | return this.set(tv) 28 | } 29 | 30 | func (this *tiktok) set(tv *Tv) error { 31 | defer func() { 32 | if err := recover(); err != nil { 33 | log.Println("tiktok panic: ", err) 34 | } 35 | }() 36 | 37 | tiktok := gotiktoklive.NewTikTok() 38 | info, err := tiktok.GetRoomInfo(tv.RoomID) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | candi := []string{ 44 | info.StreamURL.FlvPullURL.FullHd1, 45 | info.StreamURL.FlvPullURL.Hd1, 46 | info.StreamURL.FlvPullURL.Sd1, 47 | info.StreamURL.FlvPullURL.Sd2, 48 | } 49 | var streamUrl string 50 | for _, v := range candi { 51 | if v != "" { 52 | streamUrl = v 53 | break 54 | } 55 | } 56 | 57 | if streamUrl != "" { 58 | tv.roomName = info.Owner.Nickname + " is LIVE now" 59 | tv.streamerName = info.Owner.Nickname 60 | tv.roomOn = true 61 | tv.streamUrl = streamUrl 62 | } 63 | 64 | return nil 65 | } 66 | 67 | // Permit parse the stream url to get streamer info. 68 | // eg. https://www.tiktok.com/@maki_1414 69 | func (this *tiktok) Permit(roomUrl RoomUrl) (*Tv, error) { 70 | tv, error := this.base.Permit(roomUrl) 71 | if error != nil { 72 | return nil, error 73 | } 74 | tv.RoomID = strings.TrimPrefix(tv.RoomID, "@") 75 | return tv, nil 76 | } 77 | -------------------------------------------------------------------------------- /tv.go: -------------------------------------------------------------------------------- 1 | package tv 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/url" 7 | "strings" 8 | 9 | "golang.org/x/net/publicsuffix" 10 | ) 11 | 12 | const ( 13 | EmptyRoomName = "" 14 | EmptyStreamerName = "" 15 | ) 16 | 17 | var ( 18 | _ ITv = (*Tv)(nil) 19 | 20 | ErrNotSupported = errors.New("streamer not supported") 21 | ErrSiteInvalid = errors.New("site invalid") 22 | ) 23 | 24 | type ITv interface { 25 | Snap() error 26 | StreamUrl() (string, bool) 27 | RoomName() (string, bool) 28 | StreamerName() (string, bool) 29 | SiteName() string 30 | } 31 | 32 | type Tv struct { 33 | SiteID string 34 | RoomID string 35 | 36 | cookie string 37 | 38 | *Info 39 | } 40 | 41 | func New(siteID, roomID string, opts ...Option) (*Tv, error) { 42 | _, valid := Sniff(siteID) 43 | if !valid { 44 | return nil, ErrNotSupported 45 | } 46 | 47 | t := &Tv{ 48 | SiteID: siteID, 49 | RoomID: roomID, 50 | } 51 | for _, opt := range opts { 52 | opt(t) 53 | } 54 | return t, nil 55 | } 56 | 57 | func NewWithUrl(roomUrl string, opts ...Option) (*Tv, error) { 58 | u := RoomUrl(roomUrl) 59 | t, err := u.Stream() 60 | if err != nil { 61 | err = fmt.Errorf("%+v (err msg = %s)", ErrNotSupported, err.Error()) 62 | return nil, err 63 | } 64 | 65 | for _, opt := range opts { 66 | opt(t) 67 | } 68 | return t, nil 69 | } 70 | 71 | type Option func(*Tv) error 72 | 73 | func SetCookie(cookie string) Option { 74 | return func(t *Tv) error { 75 | t.cookie = cookie 76 | return nil 77 | } 78 | } 79 | 80 | type Info struct { 81 | Timestamp int64 82 | 83 | streamUrl string 84 | roomOn bool 85 | roomName string 86 | streamerName string 87 | } 88 | 89 | // Snap takes the latest snapshot of the streamer info that could be retrieved individually. 90 | func (tv *Tv) Snap() error { 91 | if tv == nil { 92 | return errors.New("tv is nil") 93 | } 94 | site, ok := Sniff(tv.SiteID) 95 | if !ok { 96 | return fmt.Errorf("site(ID = %s) not supported", tv.SiteID) 97 | } 98 | return site.Snap(tv) 99 | } 100 | 101 | func (tv *Tv) SiteName() string { 102 | if tv == nil { 103 | return "" 104 | } 105 | site, ok := Sniff(tv.SiteID) 106 | if !ok { 107 | return "" 108 | } 109 | return site.Name() 110 | } 111 | 112 | func (tv *Tv) StreamUrl() (string, bool) { 113 | if tv == nil || tv.Info == nil { 114 | return "", false 115 | } 116 | return tv.streamUrl, tv.roomOn 117 | } 118 | 119 | func (tv *Tv) RoomName() (string, bool) { 120 | if tv == nil || tv.Info == nil { 121 | return "", false 122 | } 123 | return tv.roomName, tv.roomName != EmptyRoomName 124 | } 125 | 126 | func (tv *Tv) StreamerName() (string, bool) { 127 | if tv == nil || tv.Info == nil { 128 | return "", false 129 | } 130 | return tv.streamerName, tv.streamerName != EmptyStreamerName 131 | } 132 | 133 | func (tv *Tv) String() string { 134 | sb := &strings.Builder{} 135 | sb.WriteString("Powered by go-olive/tv\n") 136 | sb.WriteString(format("SiteID", tv.SiteID)) 137 | sb.WriteString(format("SiteName", tv.SiteName())) 138 | sb.WriteString(format("RoomID", tv.RoomID)) 139 | if roomName, ok := tv.RoomName(); ok { 140 | sb.WriteString(format("RoomName", roomName)) 141 | } 142 | if streamerName, ok := tv.StreamerName(); ok { 143 | sb.WriteString(format("Streamer", streamerName)) 144 | } 145 | if streamUrl, ok := tv.StreamUrl(); ok { 146 | sb.WriteString(format("StreamUrl", streamUrl)) 147 | } 148 | return sb.String() 149 | } 150 | 151 | func format(k, v string) string { 152 | return fmt.Sprintf(" %-12s%-s\n", k, v) 153 | } 154 | 155 | type RoomUrl string 156 | 157 | func (this RoomUrl) SiteID() string { 158 | u, err := url.Parse(string(this)) 159 | if err != nil { 160 | return "" 161 | } 162 | eTLDPO, err := publicsuffix.EffectiveTLDPlusOne(u.Hostname()) 163 | if err != nil { 164 | return "" 165 | } 166 | siteID := strings.Split(eTLDPO, ".")[0] 167 | return siteID 168 | } 169 | 170 | func (this RoomUrl) Stream() (*Tv, error) { 171 | site, ok := Sniff(this.SiteID()) 172 | if !ok { 173 | return nil, ErrSiteInvalid 174 | } 175 | return site.Permit(this) 176 | } 177 | -------------------------------------------------------------------------------- /twitch.go: -------------------------------------------------------------------------------- 1 | package tv 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | 8 | "github.com/go-olive/tv/util" 9 | ) 10 | 11 | func init() { 12 | registerSite("twitch", &twitch{}) 13 | } 14 | 15 | type twitch struct { 16 | base 17 | } 18 | 19 | func (this *twitch) Name() string { 20 | return "推趣" 21 | } 22 | 23 | func (this *twitch) Snap(tv *Tv) error { 24 | tv.Info = &Info{ 25 | Timestamp: time.Now().Unix(), 26 | } 27 | return this.set(tv) 28 | } 29 | 30 | func (this *twitch) set(tv *Tv) error { 31 | roomUrl := fmt.Sprintf("https://www.twitch.tv/%s", tv.RoomID) 32 | content, err := util.GetURLContent(roomUrl) 33 | if err != nil { 34 | return err 35 | } 36 | 37 | tv.roomOn = strings.Contains(content, `"isLiveBroadcast":true`) 38 | if !tv.roomOn { 39 | return nil 40 | } 41 | 42 | tv.streamUrl = roomUrl 43 | title, err := util.Match(`"description":"([^"]+)"`, content) 44 | if err != nil { 45 | return nil 46 | } 47 | 48 | tv.roomName = title 49 | 50 | return nil 51 | } 52 | -------------------------------------------------------------------------------- /util/http.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "mime/multipart" 10 | "net/http" 11 | "net/url" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | var client = &http.Client{ 17 | Timeout: 20 * time.Second, 18 | // Transport: &http.Transport{ 19 | // //控制主机的最大空闲连接数,0为没有限制 20 | // MaxIdleConns: 1000, 21 | // MaxIdleConnsPerHost: 1000, 22 | // //长连接在关闭之前,保持空闲的最长时间,0表示没限制。 23 | // IdleConnTimeout: 60 * time.Second, 24 | // }, 25 | } 26 | 27 | type HttpRequest struct { 28 | URL string 29 | Method string 30 | Param io.Reader 31 | RequestData map[string]interface{} 32 | ResponseData interface{} 33 | Header map[string]string 34 | ContentType string 35 | } 36 | 37 | func (this *HttpRequest) Send() error { 38 | param, err := this.buildParam() 39 | if err != nil { 40 | return err 41 | } 42 | req, err := http.NewRequest(this.Method, this.URL, param) 43 | if err != nil { 44 | return fmt.Errorf("create http request failed: %s", err.Error()) 45 | } 46 | 47 | req.Header.Set("Content-Type", this.ContentType) 48 | for k, v := range this.Header { 49 | req.Header.Set(k, v) 50 | } 51 | 52 | resp, err := client.Do(req) 53 | if err != nil { 54 | return fmt.Errorf("send http request failed: %s", err.Error()) 55 | } 56 | defer resp.Body.Close() 57 | 58 | switch this.ResponseData.(type) { 59 | case string: 60 | respBody, err := ioutil.ReadAll(resp.Body) 61 | if err != nil { 62 | return err 63 | } 64 | this.ResponseData = string(respBody) 65 | return nil 66 | default: 67 | return json.NewDecoder(resp.Body).Decode(this.ResponseData) 68 | } 69 | } 70 | 71 | func (this *HttpRequest) buildParam() (io.Reader, error) { 72 | switch this.ContentType { 73 | case "application/form-data": 74 | body := new(bytes.Buffer) 75 | writer := multipart.NewWriter(body) 76 | for k, v := range this.RequestData { 77 | if err := writer.WriteField(k, fmt.Sprint(v)); err != nil { 78 | return nil, fmt.Errorf("write form field %s:%s failed: %s", k, v, err.Error()) 79 | } 80 | } 81 | if err := writer.Close(); err != nil { 82 | return nil, fmt.Errorf("close multipart form writer failed: %s", err.Error()) 83 | } 84 | this.ContentType = writer.FormDataContentType() 85 | return body, nil 86 | case "application/x-www-form-urlencoded": 87 | body := make(url.Values) 88 | for k, v := range this.RequestData { 89 | body[k] = []string{fmt.Sprint(v)} 90 | } 91 | paramData := strings.NewReader(body.Encode()) 92 | return paramData, nil 93 | case "application/json": 94 | jsonParamBytes, err := json.Marshal(this.RequestData) 95 | if err != nil { 96 | return nil, fmt.Errorf("Marshal json error:%s ", err.Error()) 97 | } 98 | paramData := bytes.NewBuffer(jsonParamBytes) 99 | return paramData, nil 100 | default: 101 | return nil, fmt.Errorf("ContentType = %s not supported", this.ContentType) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | "errors" 7 | "io/ioutil" 8 | "net/http" 9 | "regexp" 10 | ) 11 | 12 | func GetURLContent(url string) (string, error) { 13 | webUserAgent := "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36" 14 | req, err := http.NewRequest("GET", url, nil) 15 | if err != nil { 16 | return "", err 17 | } 18 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 19 | req.Header.Set("User-Agent", webUserAgent) 20 | resp, err := http.DefaultClient.Do(req) 21 | if err != nil { 22 | return "", err 23 | } 24 | defer resp.Body.Close() 25 | respBody, err := ioutil.ReadAll(resp.Body) 26 | if err != nil { 27 | return "", err 28 | } 29 | content := string(respBody) 30 | return content, nil 31 | } 32 | 33 | func Match(pattern, content string) (string, error) { 34 | re, err := regexp.Compile(pattern) 35 | if err != nil { 36 | return "", err 37 | } 38 | submatch := re.FindAllStringSubmatch(content, -1) 39 | res := make([]string, 0) 40 | for _, v := range submatch { 41 | res = append(res, string(v[1])) 42 | } 43 | if len(res) < 1 { 44 | return "", errors.New("pattern not found") 45 | } 46 | return res[0], nil 47 | } 48 | 49 | func GetMd5Hash(text string) string { 50 | hash := md5.Sum([]byte(text)) 51 | return hex.EncodeToString(hash[:]) 52 | } 53 | -------------------------------------------------------------------------------- /youtube.go: -------------------------------------------------------------------------------- 1 | package tv 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | 8 | "github.com/go-olive/tv/util" 9 | ) 10 | 11 | func init() { 12 | registerSite("youtube", &youtube{}) 13 | } 14 | 15 | type youtube struct { 16 | base 17 | } 18 | 19 | func (this *youtube) Name() string { 20 | return "油管" 21 | } 22 | 23 | func (this *youtube) Snap(tv *Tv) error { 24 | tv.Info = &Info{ 25 | Timestamp: time.Now().Unix(), 26 | } 27 | 28 | streamID, err := this.setRoomOn(tv) 29 | if err != nil { 30 | return err 31 | } 32 | return this.setStreamURL(tv, streamID) 33 | } 34 | 35 | func (this *youtube) setRoomOn(tv *Tv) (string, error) { 36 | channelURL := fmt.Sprintf("https://www.youtube.com/channel/%s", tv.RoomID) 37 | content, err := util.GetURLContent(channelURL) 38 | if err != nil { 39 | return "", err 40 | } 41 | tv.roomOn = strings.Contains(content, `icon":{"iconType":"LIVE"}}`) 42 | if !tv.roomOn { 43 | return "", nil 44 | } 45 | streamID, err := util.Match(`"videoRenderer":{"videoId":"([^"]+)",`, content) 46 | if err != nil { 47 | return "", err 48 | } 49 | return streamID, nil 50 | } 51 | 52 | func (this *youtube) setStreamURL(tv *Tv, streamID string) error { 53 | if !tv.roomOn { 54 | return nil 55 | } 56 | // youtube possibly have multiple lives in one channel, 57 | // curruently the program returns the first one. 58 | roomURL := fmt.Sprintf("https://www.youtube.com/watch?v=%s", streamID) 59 | tv.streamUrl = roomURL 60 | roomContent, err := util.GetURLContent(roomURL) 61 | if err != nil { 62 | return err 63 | } 64 | title, err := util.Match(`name="title" content="([^"]+)"`, roomContent) 65 | if err != nil { 66 | return err 67 | } 68 | tv.roomName = title 69 | return nil 70 | } 71 | --------------------------------------------------------------------------------