Refactor (#43)
* refactor * unit test * govendor -> go mod (Close: #37), go 1.12.9 -> 1.13 * add Referer header, fix zhanqi tv * Fix bilibili live resolution Missing 1080p resolution by default since bilibili updated API according to https://github.com/streamlink/streamlink/pull/2625 * recorder manager test, refine base live, refine cache * String Filter
This commit is contained in:
1 parent
9f101f7c9f
commit
f53ff00ef6
559 files changed
+3473
-228903
No files matched your search
+16
-4
@@ -1,8 +1,20 @@
|
||||
language: go
|
||||
|
||||
sudo: required
|
||||
go:
|
||||
- master
|
||||
|
||||
- "1.13"
|
||||
env:
|
||||
- GO111MODULE=on
|
||||
script:
|
||||
- make
|
||||
- make test
|
||||
- make test
|
||||
before_deploy:
|
||||
- make release
|
||||
deploy:
|
||||
provider: releases
|
||||
api_key:
|
||||
secure: xfefGaJVPyP2y+wvtosX7c0YmNw85ANfCIrUyfIY56++XlKLNSTUl3l38pXV0D9/MZyk4jOj+UPqfPoUskjB9uX7oVLLH3X7ESh/XlqP0J+VVxpVtL+iYyZ4BkuxzolZFV8nMJ5X6XfUnzG4Z3FFJUagpQN4acfX+WAP2zw5ILGrdcgWwun3kLvi2SuybAApHhEzsty7wOdmczVQYKLkU066jtG2Zs1gPl1IVg4k/U8mVEgiHwZdsB4QTg01lL4+qPd9xC7f+EHY33FQ+o4MkO6ZKnR3VjKfS4ufJp6KoVdat/m9RP8ykDpVL3n0fKtu+bYWjvetxGroFm75es8lD5vKpXAE/8mKLI+MltJgknzJlOep/i38i+6SDHZhLLa+6uCgNSZxd7ie5f2rsrFHp7X8pnevuUS4jGU/XwKuoKF9fGRa3AA6w3QEgWkn+sjECk9zyk9c5ADNTmoNaux6kLjg+acTYQHpnIApQEsGd/WK0bdCibs93FFqPQrXpr29gtC+w3ivSSzevVSNLdDmwm6AgtJbNAS+1M/hXAQDo7pZ5sxpoXAuZ2c3jAgz7htEolz0QCZX3SwMEaBQuem5jMW/nbrFGfrhN6y4a4vYr9z3V+VAhdF5q+s67kHs7+p1uZai/d8uPu2e17Yj7sIDbPPk4LU/gsrJKCCP9bgcT3k=
|
||||
skip_cleanup: true
|
||||
file_glob: true
|
||||
file: "bin/*"
|
||||
on:
|
||||
tags: true
|
||||
@@ -1,102 +1,36 @@
|
||||
BIN_PATH=./bin
|
||||
BIN_NAME=bililive-go
|
||||
VERSION=$(shell git describe --tags --always)
|
||||
BUILD_TIME=$(shell date '+%Y-%m-%d_%H:%M:%S')
|
||||
GIT_HASH=$(shell git rev-parse HEAD)
|
||||
CONSTS_PATH=github.com/hr3lxphr6j/bililive-go/src/consts
|
||||
LD_FLAGS=-ldflags '-s -w -X ${CONSTS_PATH}.BuildTime=${BUILD_TIME} -X ${CONSTS_PATH}.AppVersion=${VERSION} -X ${CONSTS_PATH}.GitHash=${GIT_HASH}'
|
||||
UPX=1
|
||||
UPX_ENABLE ?= 0
|
||||
PLATFORM ?= $(shell go env GOHOSTOS)
|
||||
ARCH ?= $(shell go env GOHOSTARCH)
|
||||
|
||||
.PHONY: all
|
||||
all: mkdir
|
||||
go build -o '${BIN_PATH}/${BIN_NAME}' ${LD_FLAGS} ./
|
||||
|
||||
.PHONY: mkdir
|
||||
mkdir:
|
||||
@mkdir -p ${BIN_PATH}
|
||||
.PHONY: $(notdir $(abspath $(wildcard src/cmd/*/)))
|
||||
local_go_version := $(shell go version | cut -d' ' -f3 | sed -e 's/go//g')
|
||||
$(notdir $(abspath $(wildcard src/cmd/*/))):
|
||||
@echo "building $@ (Platform: $(PLATFORM), Arch: $(ARCH), GoVersion: $(local_go_version))"
|
||||
@GOOS=$(PLATFORM) \
|
||||
GOARCH=$(ARCH) \
|
||||
CGO_ENABLED=0 \
|
||||
GOFLAGS=$(GOFLAGS) \
|
||||
UPX_ENABLE=$(UPX_ENABLE) \
|
||||
./src/hack/build.sh $@
|
||||
|
||||
.PHONY: release-all-platform-package
|
||||
release-all-platform-package: release-all-platform
|
||||
for file in `ls ${BIN_PATH}`; \
|
||||
do \
|
||||
7z a ${BIN_PATH}/$${file%.*}.7z ${BIN_PATH}/$${file} ./config.yml; \
|
||||
rm ${BIN_PATH}/$${file}; \
|
||||
done
|
||||
|
||||
.PHONY: release-all-platform
|
||||
release-all-platform: release-darwin-amd64 \
|
||||
release-linux-386 \
|
||||
release-linux-amd64 \
|
||||
release-linux-arm \
|
||||
release-linux-arm64 \
|
||||
release-windows-386 \
|
||||
release-windows-amd64
|
||||
|
||||
.PHONY: release-darwin-amd64
|
||||
release-darwin-amd64: mkdir
|
||||
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build ${LD_FLAGS} -o ${BIN_PATH}/${BIN_NAME}-darwin-amd64
|
||||
@if [ ${UPX} -eq 1 ]; \
|
||||
then \
|
||||
upx --no-progress ${BIN_PATH}/${BIN_NAME}-darwin-amd64; \
|
||||
fi
|
||||
|
||||
.PHONY: release-linux-386
|
||||
release-linux-386: mkdir
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=386 go build ${LD_FLAGS} -o ${BIN_PATH}/${BIN_NAME}-linux-386
|
||||
@if [ ${UPX} -eq 1 ]; \
|
||||
then \
|
||||
upx --no-progress ${BIN_PATH}/${BIN_NAME}-linux-386; \
|
||||
fi
|
||||
|
||||
.PHONY: release-linux-amd64
|
||||
release-linux-amd64: mkdir
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build ${LD_FLAGS} -o ${BIN_PATH}/${BIN_NAME}-linux-amd64
|
||||
@if [ ${UPX} -eq 1 ]; \
|
||||
then \
|
||||
upx --no-progress ${BIN_PATH}/${BIN_NAME}-linux-amd64; \
|
||||
fi
|
||||
|
||||
.PHONY: release-linux-arm
|
||||
release-linux-arm: mkdir
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm go build ${LD_FLAGS} -o ${BIN_PATH}/${BIN_NAME}-linux-arm
|
||||
@if [ ${UPX} -eq 1 ]; \
|
||||
then \
|
||||
upx --no-progress ${BIN_PATH}/${BIN_NAME}-linux-arm; \
|
||||
fi
|
||||
|
||||
.PHONY: release-linux-arm64
|
||||
release-linux-arm64: mkdir
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build ${LD_FLAGS} -o ${BIN_PATH}/${BIN_NAME}-linux-arm64
|
||||
@if [ ${UPX} -eq 1 ]; \
|
||||
then \
|
||||
upx --no-progress ${BIN_PATH}/${BIN_NAME}-linux-arm64; \
|
||||
fi
|
||||
|
||||
.PHONY: release-windows-386
|
||||
release-windows-386: mkdir
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=386 go build ${LD_FLAGS} -o ${BIN_PATH}/${BIN_NAME}-windows-386.exe
|
||||
@if [ ${UPX} -eq 1 ]; \
|
||||
then \
|
||||
upx --no-progress ${BIN_PATH}/${BIN_NAME}-windows-386.exe; \
|
||||
fi
|
||||
|
||||
.PHONY: release-windows-amd64
|
||||
release-windows-amd64: mkdir
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build ${LD_FLAGS} -o ${BIN_PATH}/${BIN_NAME}-windows-amd64.exe
|
||||
@if [ ${UPX} -eq 1 ]; \
|
||||
then \
|
||||
upx --no-progress ${BIN_PATH}/${BIN_NAME}-windows-amd64.exe; \
|
||||
fi
|
||||
.PHONY: release
|
||||
release:
|
||||
@./src/hack/release.sh
|
||||
|
||||
.PHONY: release-docker
|
||||
release-docker: release-linux-amd64
|
||||
docker build -t 'chigusa/bililive-go' .
|
||||
release-docker: clean
|
||||
@./src/hack/release-docker.sh
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
@go test --cover ./src/api
|
||||
@go test --cover ./src/...
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
@rm -rf ${BIN_PATH}
|
||||
@echo "All clean"
|
||||
@rm -rf bin
|
||||
@echo "All clean"
|
||||
|
||||
.PHONY: generate
|
||||
generate:
|
||||
go generate ./...
|
||||
-140
@@ -1,140 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/alecthomas/kingpin"
|
||||
|
||||
"github.com/hr3lxphr6j/bililive-go/src/api"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/configs"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/consts"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/instance"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/lib/events"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/lib/utils"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/listeners"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/log"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/recorders"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/servers"
|
||||
)
|
||||
|
||||
var (
|
||||
app = kingpin.New(consts.AppName, "A command-line live stream save tools.").Version(consts.AppVersion)
|
||||
debug = app.Flag("debug", "Enable debug mode.").Default("false").Bool()
|
||||
interval = app.Flag("interval", "Interval of query live status").Default("20").Short('t').Int()
|
||||
output = app.Flag("output", "Output file path.").Short('o').Default("./").String()
|
||||
input = app.Flag("input", "Live room urls").Short('i').Strings()
|
||||
conf = app.Flag("config", "Config file.").Short('c').String()
|
||||
rpc = app.Flag("enable-rpc", "Enable RPC server.").Default("false").Bool()
|
||||
rpcAddr = app.Flag("rpc-addr", "RPC server listen port").Default(":8080").String()
|
||||
rpcToken = app.Flag("rpc-token", "RPC server token.").String()
|
||||
rpcTLS = app.Flag("enable-rpc-tls", "Enable TLS for RPC server").Bool()
|
||||
certFile = app.Flag("rpc-tls-cert-file", "Cert file for TLS on RPC").String()
|
||||
keyFile = app.Flag("rpc-tls-key-file", "Key file for TLS on RPC").String()
|
||||
)
|
||||
|
||||
func getConfig() (*configs.Config, error) {
|
||||
kingpin.MustParse(app.Parse(os.Args[1:]))
|
||||
var config *configs.Config
|
||||
if *conf != "" {
|
||||
if c, err := configs.NewConfigWithFile(*conf); err == nil {
|
||||
config = c
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
config = &configs.Config{
|
||||
RPC: configs.RPC{
|
||||
Enable: *rpc,
|
||||
Port: *rpcAddr,
|
||||
Token: *rpcToken,
|
||||
TLS: configs.TLS{
|
||||
Enable: *rpcTLS,
|
||||
CertFile: *certFile,
|
||||
KeyFile: *keyFile,
|
||||
},
|
||||
},
|
||||
Debug: *debug,
|
||||
Interval: *interval,
|
||||
OutPutPath: *output,
|
||||
LiveRooms: *input,
|
||||
}
|
||||
}
|
||||
if err := configs.VerifyConfig(config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
if !utils.IsFFmpegExist() {
|
||||
fmt.Fprintf(os.Stderr, "FFmpeg binary not found, Please Check.\n")
|
||||
os.Exit(3)
|
||||
}
|
||||
|
||||
config, err := getConfig()
|
||||
if err != nil {
|
||||
fmt.Fprint(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
inst := new(instance.Instance)
|
||||
inst.Config = config
|
||||
ctx := context.WithValue(context.Background(), instance.InstanceKey, inst)
|
||||
|
||||
logger := log.NewLogger(ctx)
|
||||
logger.Infof("%s Version: %s Link Start", consts.AppName, consts.AppVersion)
|
||||
logger.Debugf("%+v", consts.AppInfo)
|
||||
logger.Debugf("%+v", inst.Config)
|
||||
|
||||
events.NewIEventDispatcher(ctx)
|
||||
|
||||
inst.Lives = make(map[api.LiveId]api.Live)
|
||||
for _, room := range inst.Config.LiveRooms {
|
||||
u, err := url.Parse(room)
|
||||
if err != nil {
|
||||
logger.WithField("url", room).Error(err)
|
||||
}
|
||||
if l, err := api.NewLive(u); err == nil {
|
||||
if _, ok := inst.Lives[l.GetLiveId()]; ok {
|
||||
logger.Errorf("%s is exist!", room)
|
||||
} else {
|
||||
inst.Lives[l.GetLiveId()] = l
|
||||
}
|
||||
} else {
|
||||
logger.WithField("url", room).Error(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if inst.Config.RPC.Enable {
|
||||
servers.NewServer(ctx).Start(ctx)
|
||||
}
|
||||
lm := listeners.NewIListenerManager(ctx)
|
||||
recorders.NewIRecorderManager(ctx)
|
||||
inst.ListenerManager.Start(ctx)
|
||||
inst.RecorderManager.Start(ctx)
|
||||
|
||||
for _, live := range inst.Lives {
|
||||
if err := lm.AddListener(ctx, live); err != nil {
|
||||
logger.WithFields(map[string]interface{}{"url": live.GetRawUrl()}).Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
c := make(chan os.Signal)
|
||||
signal.Notify(c, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
|
||||
go func() {
|
||||
<-c
|
||||
if inst.Config.RPC.Enable {
|
||||
inst.Server.Close(ctx)
|
||||
}
|
||||
inst.ListenerManager.Close(ctx)
|
||||
inst.RecorderManager.Close(ctx)
|
||||
}()
|
||||
|
||||
inst.WaitGroup.Wait()
|
||||
logger.Info("Bye~")
|
||||
}
|
||||
+1
-1
@@ -8,7 +8,7 @@ rpc:
|
||||
key_file: ""
|
||||
debug: false
|
||||
interval: 15
|
||||
out_put_path: /Users/chigusa/Movies
|
||||
out_put_path: ./
|
||||
feature:
|
||||
use_native_flv_parser: false
|
||||
live_rooms:
|
||||
@@ -0,0 +1,27 @@
|
||||
module github.com/hr3lxphr6j/bililive-go
|
||||
|
||||
go 1.13
|
||||
|
||||
require (
|
||||
github.com/alecthomas/kingpin v2.2.7-0.20180312062423-a39589180ebd+incompatible
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc // indirect
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf // indirect
|
||||
github.com/bluele/gcache v0.0.0-20190518031135-bc40bd653833
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/golang/mock v1.3.2-0.20190713102442-dd8d2a22370e
|
||||
github.com/gorilla/context v1.1.1 // indirect
|
||||
github.com/gorilla/mux v1.6.2-0.20180501031136-ded0c29b24f9
|
||||
github.com/onsi/ginkgo v1.10.1 // indirect
|
||||
github.com/onsi/gomega v1.7.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/robertkrimen/otto v0.0.0-20180617131154-15f95af6e78d
|
||||
github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b
|
||||
github.com/sirupsen/logrus v1.0.6-0.20180329225952-778f2e774c72
|
||||
github.com/stretchr/testify v1.4.0
|
||||
github.com/tidwall/gjson v1.0.6
|
||||
github.com/tidwall/match v1.0.0 // indirect
|
||||
gopkg.in/airbrake/gobrake.v2 v2.0.9 // indirect
|
||||
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 // indirect
|
||||
gopkg.in/sourcemap.v1 v1.0.5 // indirect
|
||||
gopkg.in/yaml.v2 v2.2.2
|
||||
)
|
||||
@@ -0,0 +1,73 @@
|
||||
github.com/alecthomas/kingpin v2.2.7-0.20180312062423-a39589180ebd+incompatible h1:aDLzaG6QPT5c7ug8eTPaNbWqyEI9dzMY9OzPrlKydSw=
|
||||
github.com/alecthomas/kingpin v2.2.7-0.20180312062423-a39589180ebd+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/bluele/gcache v0.0.0-20190518031135-bc40bd653833 h1:yCfXxYaelOyqnia8F/Yng47qhmfC9nKTRIbYRrRueq4=
|
||||
github.com/bluele/gcache v0.0.0-20190518031135-bc40bd653833/go.mod h1:8c4/i2VlovMO2gBnHGQPN5EJw+H0lx1u/5p+cgsXtCk=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/golang/mock v1.3.2-0.20190713102442-dd8d2a22370e h1:M0vqwaXA0SBjFssWzqd+KxcqD/JKc+fnwq+q89DLYuk=
|
||||
github.com/golang/mock v1.3.2-0.20190713102442-dd8d2a22370e/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/mux v1.6.2-0.20180501031136-ded0c29b24f9 h1:PaHZOkKXjo5QHqXwKMjDQz8fwkcxqTXgEs+nyOF9M2I=
|
||||
github.com/gorilla/mux v1.6.2-0.20180501031136-ded0c29b24f9/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo=
|
||||
github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME=
|
||||
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/robertkrimen/otto v0.0.0-20180617131154-15f95af6e78d h1:1VUlQbCfkoSGv7qP7Y+ro3ap1P1pPZxgdGVqiTVy5C4=
|
||||
github.com/robertkrimen/otto v0.0.0-20180617131154-15f95af6e78d/go.mod h1:xvqspoSXJTIpemEonrMDFq6XzwHYYgToXWj5eRX1OtY=
|
||||
github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b h1:gQZ0qzfKHQIybLANtM3mBXNUtOfsCFXeTsnBqCsx1KM=
|
||||
github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/sirupsen/logrus v1.0.6-0.20180329225952-778f2e774c72 h1:Q+ZnVIwiN5ZgpKd4h/7rUiJgD5pQx6WYlcvHw4yUaAs=
|
||||
github.com/sirupsen/logrus v1.0.6-0.20180329225952-778f2e774c72/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/tidwall/gjson v1.0.6 h1:HYH2srVkCC53232wJXWptchlbF1n2JxJBmM8i1/bLXU=
|
||||
github.com/tidwall/gjson v1.0.6/go.mod h1:c/nTNbUr0E0OrXEhq1pwa8iEgc2DOt4ZZqAt1HtCkPA=
|
||||
github.com/tidwall/match v1.0.0 h1:Ym1EcFkp+UQ4ptxfWlW+iMdq5cPH5nEuGzdf/Pb7VmI=
|
||||
github.com/tidwall/match v1.0.0/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
gopkg.in/airbrake/gobrake.v2 v2.0.9 h1:7z2uVWwn7oVeeugY1DtlPAy5H+KYgB1KeKTnqjNatLo=
|
||||
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 h1:OAj3g0cR6Dx/R07QgQe8wkA9RNjB2u4i700xBkIT4e0=
|
||||
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
|
||||
gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI=
|
||||
gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
@@ -1,47 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
type abstractLive struct {
|
||||
Url *url.URL
|
||||
lastStartTime time.Time
|
||||
cachedInfo *Info
|
||||
liveId LiveId
|
||||
}
|
||||
|
||||
func (a *abstractLive) GetLiveId() LiveId {
|
||||
return a.liveId
|
||||
}
|
||||
|
||||
func (a *abstractLive) GetRawUrl() string {
|
||||
return a.Url.String()
|
||||
}
|
||||
|
||||
func (a *abstractLive) GetCachedInfo() *Info {
|
||||
return a.cachedInfo
|
||||
}
|
||||
|
||||
func (a *abstractLive) GetInfoMap() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"id": a.GetLiveId(),
|
||||
"url": a.GetRawUrl(),
|
||||
"host_name": a.GetCachedInfo().HostName,
|
||||
"room_name": a.GetCachedInfo().RoomName,
|
||||
"status": a.GetCachedInfo().Status,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *abstractLive) GetPlatformCNName() string {
|
||||
return LivePlatformCNNameMap[a.Url.Host]
|
||||
}
|
||||
|
||||
func (a *abstractLive) GetLastStartTime() time.Time {
|
||||
return a.lastStartTime
|
||||
}
|
||||
|
||||
func (a *abstractLive) SetLastStartTime(time time.Time) {
|
||||
a.lastStartTime = time
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/hr3lxphr6j/bililive-go/src/lib/http"
|
||||
)
|
||||
|
||||
const (
|
||||
biliBiliRoomInitUrl = "https://api.live.bilibili.com/room/v1/Room/room_init"
|
||||
biliBiliRoomApiUrl = "https://api.live.bilibili.com/room/v1/Room/get_info"
|
||||
biliBiliUserApiUrl = "https://api.live.bilibili.com/live_user/v1/UserInfo/get_anchor_in_room"
|
||||
biliBiliLiveApiUrl = "https://api.live.bilibili.com/room/v1/Room/playUrl"
|
||||
)
|
||||
|
||||
type BiliBiliLive struct {
|
||||
abstractLive
|
||||
realId string
|
||||
}
|
||||
|
||||
func (b *BiliBiliLive) parseRealId() error {
|
||||
if body, err := http.Get(biliBiliRoomInitUrl, map[string]string{"id": strings.Split(b.Url.Path, "/")[1]}, nil); err != nil {
|
||||
return err
|
||||
} else {
|
||||
if gjson.GetBytes(body, "code").Int() != 0 {
|
||||
return &RoomNotExistsError{b.Url}
|
||||
} else {
|
||||
b.realId = gjson.GetBytes(body, "data.room_id").String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BiliBiliLive) GetInfo() (info *Info, err error) {
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
err = e.(error)
|
||||
}
|
||||
}()
|
||||
|
||||
// Parse the short id from URL to full id
|
||||
if b.realId == "" {
|
||||
if err := b.parseRealId(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
body, err := http.Get(biliBiliRoomApiUrl, map[string]string{
|
||||
"room_id": b.realId,
|
||||
"from": "room",
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if gjson.GetBytes(body, "code").Int() != 0 {
|
||||
return nil, &RoomNotExistsError{b.Url}
|
||||
}
|
||||
|
||||
info = &Info{
|
||||
Live: b,
|
||||
RoomName: gjson.GetBytes(body, "data.title").String(),
|
||||
Status: gjson.GetBytes(body, "data.live_status").Int() == 1,
|
||||
}
|
||||
|
||||
body2, err := http.Get(biliBiliUserApiUrl, map[string]string{
|
||||
"roomid": b.realId,
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
info.HostName = gjson.GetBytes(body2, "data.info.uname").String()
|
||||
b.cachedInfo = info
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (b *BiliBiliLive) GetStreamUrls() (us []*url.URL, err error) {
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
err = e.(error)
|
||||
}
|
||||
}()
|
||||
|
||||
if b.realId == "" {
|
||||
if err := b.parseRealId(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
body, err := http.Get(biliBiliLiveApiUrl, map[string]string{
|
||||
"cid": b.realId,
|
||||
"quality": "4",
|
||||
"platform": "web",
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
urls := gjson.GetBytes(body, "data.durl.#.url").Array()
|
||||
|
||||
us = make([]*url.URL, 0, 4)
|
||||
for _, u := range urls {
|
||||
url_, _ := url.Parse(u.String())
|
||||
us = append(us, url_)
|
||||
}
|
||||
return us, nil
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const biliBiliTestUrl = "https://live.bilibili.com/161"
|
||||
|
||||
func TestBiliBiliLive_GetInfo(t *testing.T) {
|
||||
u, _ := url.Parse(biliBiliTestUrl)
|
||||
live, _ := NewLive(u)
|
||||
t.Log(live.GetInfo())
|
||||
}
|
||||
|
||||
func TestBiliBiliLive_GetStreamUrls(t *testing.T) {
|
||||
u, _ := url.Parse(biliBiliTestUrl)
|
||||
live, _ := NewLive(u)
|
||||
t.Log(live.GetStreamUrls())
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"regexp"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/hr3lxphr6j/bililive-go/src/lib/http"
|
||||
)
|
||||
|
||||
const (
|
||||
ccLiveApiUrl = "http://cgi.v.cc.163.com/video_play_url/"
|
||||
)
|
||||
|
||||
type CCLive struct {
|
||||
abstractLive
|
||||
ccid string
|
||||
}
|
||||
|
||||
func (c *CCLive) GetInfo() (info *Info, err error) {
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
err = e.(error)
|
||||
}
|
||||
}()
|
||||
|
||||
dom, err := http.Get(c.Url.String(), nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.ccid = regexp.MustCompile(`anchorCcId\s*:\s*'(\d*)'`).FindStringSubmatch(string(dom))[1]
|
||||
info = &Info{
|
||||
Live: c,
|
||||
HostName: regexp.MustCompile(`anchorName\s*:\s*'([^']*)',`).FindStringSubmatch(string(dom))[1],
|
||||
RoomName: regexp.MustCompile(`js-live-title nick" title\s*=\s*"([^"]*)"`).FindStringSubmatch(string(dom))[1],
|
||||
Status: len(regexp.MustCompile(`isLive\s*:\s*\d+,`).FindAll(dom, -1)) > 0,
|
||||
}
|
||||
c.cachedInfo = info
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (c *CCLive) GetStreamUrls() (us []*url.URL, err error) {
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
err = e.(error)
|
||||
}
|
||||
}()
|
||||
data, err := http.Get(fmt.Sprintf("%s%s", ccLiveApiUrl, c.ccid), nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
us = make([]*url.URL, 0)
|
||||
u0, _ := url.Parse(gjson.GetBytes(data, "videourl").String())
|
||||
u1, _ := url.Parse(gjson.GetBytes(data, "bakvideourl").String())
|
||||
us = append(us, u0, u1)
|
||||
return us, nil
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const ccTestUrl = "https://cc.163.com/361433"
|
||||
|
||||
func TestCCLive_GetInfo(t *testing.T) {
|
||||
u, _ := url.Parse(ccTestUrl)
|
||||
live, _ := NewLive(u)
|
||||
t.Log(live.GetInfo())
|
||||
}
|
||||
|
||||
func TestCCLive_GetStreamUrls(t *testing.T) {
|
||||
u, _ := url.Parse(ccTestUrl)
|
||||
live, _ := NewLive(u)
|
||||
t.Log(live.GetStreamUrls())
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const douyuTestUrl = "https://www.douyu.com/6655"
|
||||
|
||||
func TestDouyuLive_GetInfo(t *testing.T) {
|
||||
u, _ := url.Parse(douyuTestUrl)
|
||||
live, _ := NewLive(u)
|
||||
t.Log(live.GetInfo())
|
||||
}
|
||||
|
||||
func TestDouyuLive_GetStreamUrls(t *testing.T) {
|
||||
u, _ := url.Parse(douyuTestUrl)
|
||||
live, _ := NewLive(u)
|
||||
t.Log(live.GetStreamUrls())
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type RoomNotExistsError struct {
|
||||
Url *url.URL
|
||||
}
|
||||
|
||||
func (e *RoomNotExistsError) Error() string {
|
||||
return "room not exists"
|
||||
}
|
||||
|
||||
func IsRoomNotExistsError(err error) bool {
|
||||
_, ok := err.(*RoomNotExistsError)
|
||||
return ok
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/hr3lxphr6j/bililive-go/src/lib/http"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/lib/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
huomaoLiveApiUrl = "http://www.huomao.com/swf/live_data"
|
||||
huomaoSalt = "6FE26D855E1AEAE090E243EB1AF73685"
|
||||
)
|
||||
|
||||
type HuoMaoLive struct {
|
||||
abstractLive
|
||||
isDuanbo bool
|
||||
}
|
||||
|
||||
func (h *HuoMaoLive) GetInfo() (info *Info, err error) {
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
err = e.(error)
|
||||
}
|
||||
}()
|
||||
|
||||
dom, err := http.Get(h.Url.String(), nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h.isDuanbo = regexp.MustCompile(`face_label\s?=\s?(\d*);`).FindStringSubmatch(string(dom))[1] == "1"
|
||||
var hostNameReg string
|
||||
var roomNameReg string
|
||||
var statusReg string
|
||||
if h.isDuanbo {
|
||||
hostNameReg = `live_yz_h_nickName\s?=\s?"([^"]*)";`
|
||||
roomNameReg = `live_yz_h_channelName\s?=\s?"([^"]*)";`
|
||||
statusReg = `is_live\s?=\s?"?(\d*)"?;`
|
||||
} else {
|
||||
hostNameReg = `"nickname":"([^"]*)",`
|
||||
roomNameReg = `"channel":"([^"]*)"`
|
||||
statusReg = `"is_live":"?(\d*)"?,`
|
||||
}
|
||||
info = &Info{
|
||||
Live: h,
|
||||
HostName: utils.ParseUnicode(regexp.MustCompile(hostNameReg).FindStringSubmatch(string(dom))[1]),
|
||||
RoomName: utils.ParseUnicode(regexp.MustCompile(roomNameReg).FindStringSubmatch(string(dom))[1]),
|
||||
Status: utils.ParseUnicode(regexp.MustCompile(statusReg).FindStringSubmatch(string(dom))[1]) == "1",
|
||||
}
|
||||
h.cachedInfo = info
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (h *HuoMaoLive) GetStreamUrls() ([]*url.URL, error) {
|
||||
dom, err := http.Get(h.Url.String(), nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var streamReg string
|
||||
if !h.isDuanbo {
|
||||
streamReg = `"stream":"([^"]*)"`
|
||||
} else {
|
||||
streamReg = `getFlash\("\d*","([^"]*)","\d*"\);`
|
||||
}
|
||||
from := "huomaoh5room"
|
||||
t := fmt.Sprintf("%d", time.Now().Unix())
|
||||
streamID := regexp.MustCompile(streamReg).FindStringSubmatch(string(dom))[1]
|
||||
token := utils.GetMd5String([]byte(fmt.Sprintf("%s%s%s%s", streamID, from, t, huomaoSalt)))
|
||||
body, err := http.Post(huomaoLiveApiUrl, map[string]string{
|
||||
"VideoIDS": streamID,
|
||||
"streamtype": "live",
|
||||
"cdns": "1",
|
||||
"from": from,
|
||||
"time": t,
|
||||
"token": token,
|
||||
}, nil, nil)
|
||||
us := make([]*url.URL, 0, 4)
|
||||
gjson.GetBytes(body, "streamList.#.list.#.url").ForEach(func(key, value gjson.Result) bool {
|
||||
for _, u := range value.Array() {
|
||||
url_, _ := url.Parse(u.String())
|
||||
us = append(us, url_)
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
return us, nil
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const (
|
||||
huomaoTestUrl = "https://www.huomao.com/6710" // 正常直播间
|
||||
huomaoTestUrl2 = "https://www.huomao.com/954927" // 娱乐直播间
|
||||
)
|
||||
|
||||
func TestHuoMaoLive_GetInfo(t *testing.T) {
|
||||
u, _ := url.Parse(huomaoTestUrl2)
|
||||
live, _ := NewLive(u)
|
||||
t.Log(live.GetInfo())
|
||||
}
|
||||
|
||||
func TestHuoMaoLive_GetStreamUrls(t *testing.T) {
|
||||
u, _ := url.Parse(huomaoTestUrl)
|
||||
live, _ := NewLive(u)
|
||||
t.Log(live.GetStreamUrls())
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hr3lxphr6j/bililive-go/src/lib/http"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/lib/utils"
|
||||
)
|
||||
|
||||
type HuYaLive struct {
|
||||
abstractLive
|
||||
}
|
||||
|
||||
func (h *HuYaLive) GetInfo() (info *Info, err error) {
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
err = e.(error)
|
||||
}
|
||||
}()
|
||||
|
||||
dom, err := http.Get(h.Url.String(), nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if res := regexp.MustCompile("哎呀,虎牙君找不到这个主播,要不搜索看看?").FindStringSubmatch(string(dom)); res != nil {
|
||||
return nil, &RoomNotExistsError{h.Url}
|
||||
}
|
||||
info = &Info{
|
||||
Live: h,
|
||||
HostName: utils.ParseUnicode(regexp.MustCompile(`"nick":"([^"]*)"`).FindStringSubmatch(string(dom))[1]),
|
||||
RoomName: utils.ParseUnicode(regexp.MustCompile(`"introduction":"([^"]*)"`).FindStringSubmatch(string(dom))[1]),
|
||||
Status: utils.ParseUnicode(regexp.MustCompile(`"isOn":([^,]*),`).FindStringSubmatch(string(dom))[1]) == "true",
|
||||
}
|
||||
h.cachedInfo = info
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (h *HuYaLive) GetStreamUrls() (us []*url.URL, err error) {
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
err = e.(error)
|
||||
}
|
||||
}()
|
||||
|
||||
dom, err := http.Get(h.Url.String(), nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sStreamName := regexp.MustCompile(`"sStreamName":"([^"]*)"`).FindStringSubmatch(string(dom))[1]
|
||||
sFlvUrl := strings.Replace(regexp.MustCompile(`"sFlvUrl":"([^"]*)"`).FindStringSubmatch(string(dom))[1], `\/`, `/`, -1)
|
||||
sFlvAntiCode := regexp.MustCompile(`"sFlvAntiCode":"([^"]*)"`).FindStringSubmatch(string(dom))[1]
|
||||
iLineIndex := regexp.MustCompile(`"iLineIndex":(\d*),`).FindStringSubmatch(string(dom))[1]
|
||||
uid := (time.Now().Unix()%1e7*1e6 + int64(1e3*rand.Float64())) % 4294967295
|
||||
u, err := url.Parse(fmt.Sprintf("%s/%s.flv", sFlvUrl, sStreamName))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
value := &url.Values{}
|
||||
value.Add("line", iLineIndex)
|
||||
value.Add("p2p", "0")
|
||||
value.Add("type", "web")
|
||||
value.Add("ver", "1805071653")
|
||||
value.Add("uid", fmt.Sprintf("%d", uid))
|
||||
u.RawQuery = fmt.Sprintf("%s&%s", value.Encode(), strings.ReplaceAll(sFlvAntiCode, "&", "&"))
|
||||
return []*url.URL{u}, nil
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const huyaTestUrl = "https://www.huya.com/wexiamo"
|
||||
|
||||
func TestHuYaLive_GetInfo(t *testing.T) {
|
||||
u, _ := url.Parse(huyaTestUrl)
|
||||
live, _ := NewLive(u)
|
||||
t.Log(live.GetInfo())
|
||||
}
|
||||
|
||||
func TestHuYaLive_GetStreamUrls(t *testing.T) {
|
||||
u, _ := url.Parse(huyaTestUrl)
|
||||
live, _ := NewLive(u)
|
||||
info, _ := live.GetInfo()
|
||||
if info.Status {
|
||||
t.Log(live.GetStreamUrls())
|
||||
}
|
||||
}
|
||||
-121
@@ -1,121 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/hr3lxphr6j/bililive-go/src/lib/utils"
|
||||
)
|
||||
|
||||
type LiveId string
|
||||
|
||||
var LivePlatformCNNameMap = map[string]string{
|
||||
"live.bilibili.com": "哔哩哔哩",
|
||||
"www.zhanqi.tv": "战旗",
|
||||
"www.douyu.com": "斗鱼",
|
||||
"star.longzhu.com": "龙珠",
|
||||
"www.huomao.com": "火猫",
|
||||
"www.yizhibo.com": "一直播",
|
||||
"www.twitch.tv": "twitch",
|
||||
"www.huya.com": "虎牙",
|
||||
"cc.163.com": "CC直播",
|
||||
"www.openrec.tv": "openrec",
|
||||
"egame.qq.com": "企鹅电竞",
|
||||
}
|
||||
|
||||
type Info struct {
|
||||
Live Live
|
||||
HostName, RoomName string
|
||||
Status, Listening, Recoding bool
|
||||
}
|
||||
|
||||
func (i *Info) MarshalJSON() ([]byte, error) {
|
||||
t := struct {
|
||||
Id LiveId `json:"id"`
|
||||
LiveUrl string `json:"live_url"`
|
||||
PlatformCNName string `json:"platform_cn_name"`
|
||||
HostName string `json:"host_name"`
|
||||
RoomName string `json:"room_name"`
|
||||
Status bool `json:"status"`
|
||||
Listening bool `json:"listening"`
|
||||
Recoding bool `json:"recoding"`
|
||||
LastStartTime string `json:"last_start_time,omitempty"`
|
||||
LastStartTimeUnix int64 `json:"last_start_time_unix,omitempty"`
|
||||
}{
|
||||
Id: i.Live.GetLiveId(),
|
||||
LiveUrl: i.Live.GetRawUrl(),
|
||||
PlatformCNName: i.Live.GetPlatformCNName(),
|
||||
HostName: i.HostName,
|
||||
RoomName: i.RoomName,
|
||||
Status: i.Status,
|
||||
Listening: i.Listening,
|
||||
Recoding: i.Recoding,
|
||||
}
|
||||
if !i.Live.GetLastStartTime().IsZero() {
|
||||
t.LastStartTime = i.Live.GetLastStartTime().Format("2006-01-02 15:04:05")
|
||||
t.LastStartTimeUnix = i.Live.GetLastStartTime().Unix()
|
||||
}
|
||||
return json.Marshal(t)
|
||||
}
|
||||
|
||||
type Live interface {
|
||||
GetLiveId() LiveId
|
||||
GetRawUrl() string
|
||||
GetInfo() (*Info, error)
|
||||
GetInfoMap() map[string]interface{}
|
||||
GetCachedInfo() *Info
|
||||
GetStreamUrls() ([]*url.URL, error)
|
||||
GetPlatformCNName() string
|
||||
GetLastStartTime() time.Time
|
||||
SetLastStartTime(time.Time)
|
||||
}
|
||||
|
||||
func NewLive(url *url.URL) (Live, error) {
|
||||
baseLive := abstractLive{
|
||||
Url: url,
|
||||
liveId: LiveId(utils.GetMd5String([]byte(fmt.Sprintf("%s%s", url.Host, url.Path)))),
|
||||
}
|
||||
var live Live
|
||||
switch url.Host {
|
||||
case "live.bilibili.com":
|
||||
live = &BiliBiliLive{abstractLive: baseLive}
|
||||
case "www.zhanqi.tv":
|
||||
live = &ZhanQiLive{abstractLive: baseLive}
|
||||
case "www.douyu.com":
|
||||
live = &DouyuLive{abstractLive: baseLive}
|
||||
case "star.longzhu.com":
|
||||
live = &LongzhuLive{abstractLive: baseLive}
|
||||
case "www.huomao.com":
|
||||
live = &HuoMaoLive{abstractLive: baseLive}
|
||||
case "www.yizhibo.com":
|
||||
live = &YiZhiBoLive{abstractLive: baseLive}
|
||||
case "www.twitch.tv":
|
||||
live = &TwitchLive{abstractLive: baseLive}
|
||||
case "www.huya.com":
|
||||
live = &HuYaLive{abstractLive: baseLive}
|
||||
case "cc.163.com":
|
||||
live = &CCLive{abstractLive: baseLive}
|
||||
case "www.openrec.tv":
|
||||
live = &OpenRecLive{abstractLive: baseLive}
|
||||
case "egame.qq.com":
|
||||
live = &QQLive{abstractLive: baseLive}
|
||||
default:
|
||||
live = nil
|
||||
}
|
||||
if live != nil {
|
||||
for i := 0; i < 3; i++ {
|
||||
if _, err := live.GetInfo(); err != nil {
|
||||
if IsRoomNotExistsError(err) {
|
||||
return nil, err
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
} else {
|
||||
return live, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, errors.New("can not parse")
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/hr3lxphr6j/bililive-go/src/lib/http"
|
||||
)
|
||||
|
||||
const (
|
||||
longzhuMobileUrl = "http://m.longzhu.com/"
|
||||
longzhuRoomApiUrl = "http://liveapi.plu.cn/liveapp/roomstatus"
|
||||
longzhuLiveApiUrl = "http://livestream.plu.cn/live/getlivePlayurl"
|
||||
)
|
||||
|
||||
type LongzhuLive struct {
|
||||
abstractLive
|
||||
realId string
|
||||
}
|
||||
|
||||
func (l *LongzhuLive) parseRealId() error {
|
||||
dom, err := http.Get(fmt.Sprintf("%s%s", longzhuMobileUrl, strings.Split(l.Url.Path, "/")[1]), nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reg := regexp.MustCompile(`var\s*roomId\s*=\s*(\d+);`)
|
||||
realIds := reg.FindStringSubmatch(string(dom))
|
||||
if realIds == nil {
|
||||
return &RoomNotExistsError{l.Url}
|
||||
}
|
||||
l.realId = realIds[1]
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *LongzhuLive) GetInfo() (info *Info, err error) {
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
err = e.(error)
|
||||
}
|
||||
}()
|
||||
|
||||
if l.realId == "" {
|
||||
if err := l.parseRealId(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
body, err := http.Get(longzhuRoomApiUrl, map[string]string{"roomId": l.realId}, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info = &Info{
|
||||
Live: l,
|
||||
HostName: gjson.GetBytes(body, "userName").String(),
|
||||
RoomName: gjson.GetBytes(body, "title").String(),
|
||||
Status: len(gjson.GetBytes(body, "streamUri").String()) > 4,
|
||||
}
|
||||
l.cachedInfo = info
|
||||
return info, nil
|
||||
|
||||
}
|
||||
|
||||
func (l *LongzhuLive) GetStreamUrls() (us []*url.URL, err error) {
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
err = e.(error)
|
||||
}
|
||||
}()
|
||||
|
||||
if l.realId == "" {
|
||||
if err := l.parseRealId(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
body, err := http.Get(longzhuLiveApiUrl, map[string]string{"roomId": l.realId}, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
urls := gjson.GetBytes(body, "playLines.0.urls.#.securityUrl").Array()
|
||||
us = make([]*url.URL, 0, 4)
|
||||
for _, u := range urls {
|
||||
url_, _ := url.Parse(u.String())
|
||||
us = append(us, url_)
|
||||
}
|
||||
return us, nil
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const longzhuTestUrl = "http://star.longzhu.com/777777"
|
||||
|
||||
func TestLongzhuLive_GetInfo(t *testing.T) {
|
||||
u, _ := url.Parse(longzhuTestUrl)
|
||||
live, _ := NewLive(u)
|
||||
t.Log(live.GetInfo())
|
||||
}
|
||||
|
||||
func TestLongzhuLive_GetStreamUrls(t *testing.T) {
|
||||
u, _ := url.Parse(longzhuTestUrl)
|
||||
live, _ := NewLive(u)
|
||||
t.Log(live.GetStreamUrls())
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/hr3lxphr6j/bililive-go/src/lib/http"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/lib/utils"
|
||||
)
|
||||
|
||||
type OpenRecLive struct {
|
||||
abstractLive
|
||||
}
|
||||
|
||||
func (o *OpenRecLive) GetInfo() (info *Info, err error) {
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
err = e.(error)
|
||||
}
|
||||
}()
|
||||
|
||||
dom, err := http.Get(o.Url.String(), nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info = &Info{
|
||||
Live: o,
|
||||
RoomName: strings.TrimSpace(regexp.MustCompile(`"title":"([^:]*)",`).FindStringSubmatch(string(dom))[1]),
|
||||
HostName: utils.ParseUnicode(regexp.MustCompile(`"name":"([^:]*)",`).FindStringSubmatch(string(dom))[1]),
|
||||
Status: regexp.MustCompile(`"onairStatus":(\d),`).FindStringSubmatch(string(dom))[1] == "1",
|
||||
}
|
||||
o.cachedInfo = info
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (o *OpenRecLive) GetStreamUrls() (us []*url.URL, err error) {
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
err = e.(error)
|
||||
}
|
||||
}()
|
||||
|
||||
dom, err := http.Get(o.Url.String(), nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
u1, _ := url.Parse(regexp.MustCompile(`{"url":"(\S*m3u8)",`).FindStringSubmatch(string(dom))[1])
|
||||
us = []*url.URL{u1}
|
||||
return us, nil
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const openrecTestUrl = "https://www.openrec.tv/live/JEmT0qQP1BM"
|
||||
|
||||
func TestOpenRecLive_GetInfo(t *testing.T) {
|
||||
u, _ := url.Parse(openrecTestUrl)
|
||||
live, _ := NewLive(u)
|
||||
t.Log(live.GetInfo())
|
||||
}
|
||||
|
||||
func TestOpenRecLive_GetStreamUrls(t *testing.T) {
|
||||
u, _ := url.Parse(openrecTestUrl)
|
||||
live, _ := NewLive(u)
|
||||
if live.GetCachedInfo().Status {
|
||||
t.Log(live.GetStreamUrls())
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/hr3lxphr6j/bililive-go/src/lib/http"
|
||||
)
|
||||
|
||||
var (
|
||||
qqLiveMobileUrl = "https://m.egame.qq.com/live"
|
||||
qqLiveStreamsReg = regexp.MustCompile(`"urlArray":(\[[^\]]+\])`)
|
||||
qqLiveRoomNameReg = regexp.MustCompile(`title:"([^"]*)"`)
|
||||
qqLiveHostNameReg = regexp.MustCompile(`nickName:"([^"]+)"`)
|
||||
qqLiveIsLiveReg = regexp.MustCompile(`"isLive":(\d+)`)
|
||||
)
|
||||
|
||||
type QQLive struct {
|
||||
abstractLive
|
||||
}
|
||||
|
||||
func (q *QQLive) GetInfo() (info *Info, err error) {
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
err = e.(error)
|
||||
}
|
||||
}()
|
||||
|
||||
anchorID := strings.Split(q.Url.Path, "/")[1]
|
||||
dom, err := http.Get(q.Url.String(), nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
roomName := qqLiveRoomNameReg.FindSubmatch(dom)[1]
|
||||
hostName := qqLiveHostNameReg.FindSubmatch(dom)[1]
|
||||
dom2, err := http.Get(qqLiveMobileUrl, map[string]string{
|
||||
"anchorid": anchorID,
|
||||
}, nil)
|
||||
isLive := string(qqLiveIsLiveReg.FindSubmatch(dom2)[1]) == "1"
|
||||
info = &Info{
|
||||
Live: q,
|
||||
RoomName: string(roomName),
|
||||
HostName: string(hostName),
|
||||
Status: isLive,
|
||||
}
|
||||
q.cachedInfo = info
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (q *QQLive) GetStreamUrls() (us []*url.URL, err error) {
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
err = e.(error)
|
||||
}
|
||||
}()
|
||||
|
||||
dom, err := http.Get(q.Url.String(), nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := qqLiveStreamsReg.FindSubmatch(dom)
|
||||
if len(result) != 2 {
|
||||
return nil, errors.New("failed to get streams")
|
||||
}
|
||||
u, err := url.Parse(gjson.GetBytes(result[1], "#[bitrate==0].playUrl").String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []*url.URL{u}, nil
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const (
|
||||
qqTestUrl = "https://egame.qq.com/497383565"
|
||||
)
|
||||
|
||||
func TestQQ_GetInfo(t *testing.T) {
|
||||
u, _ := url.Parse(qqTestUrl)
|
||||
live, _ := NewLive(u)
|
||||
t.Log(live.GetInfo())
|
||||
}
|
||||
|
||||
func TestQQ_GetStreamUrls(t *testing.T) {
|
||||
u, _ := url.Parse(qqTestUrl)
|
||||
live, _ := NewLive(u)
|
||||
t.Log(live.GetStreamUrls())
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/hr3lxphr6j/bililive-go/src/lib/http"
|
||||
)
|
||||
|
||||
const (
|
||||
twitchClientId = "jzkbprff40iqj646a697cyrvl0zt2m6"
|
||||
twitchChannelApiUrl = "https://api.twitch.tv/kraken/channels/%s"
|
||||
twitchLiveBaseUrl = "https://usher.ttvnw.net/api/channel/hls/%s.m3u8"
|
||||
twitchStreamApiUrl = "https://api.twitch.tv/kraken/streams/%s"
|
||||
twitchTokenApiUrl = "https://api.twitch.tv/api/channels/%s/access_token"
|
||||
)
|
||||
|
||||
var twitchHeader = map[string]string{"client-id": twitchClientId}
|
||||
|
||||
type TwitchLive struct {
|
||||
abstractLive
|
||||
hostName, roomName string
|
||||
}
|
||||
|
||||
// 在hostName, roomName为空执行,在live有效时再从steam api解析
|
||||
func (t *TwitchLive) parseInfo() error {
|
||||
chanId := strings.Split(t.Url.Path, "/")[1]
|
||||
body, err := http.Get(fmt.Sprintf(twitchChannelApiUrl, chanId), nil, twitchHeader)
|
||||
if err != nil {
|
||||
return &RoomNotExistsError{t.Url}
|
||||
}
|
||||
t.hostName = gjson.GetBytes(body, "name").String()
|
||||
t.roomName = gjson.GetBytes(body, "status").String()
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (t *TwitchLive) GetInfo() (info *Info, err error) {
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
err = e.(error)
|
||||
}
|
||||
}()
|
||||
|
||||
if t.hostName == "" || t.roomName == "" {
|
||||
if err := t.parseInfo(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
body, err := http.Get(fmt.Sprintf(twitchStreamApiUrl, t.hostName), nil, twitchHeader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
status := gjson.GetBytes(body, "stream").String() != ""
|
||||
if status {
|
||||
t.roomName = gjson.GetBytes(body, "stream.channel.status").String()
|
||||
}
|
||||
info = &Info{
|
||||
Live: t,
|
||||
HostName: t.hostName,
|
||||
RoomName: t.roomName,
|
||||
Status: status,
|
||||
}
|
||||
t.cachedInfo = info
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (t *TwitchLive) GetStreamUrls() (us []*url.URL, err error) {
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
err = e.(error)
|
||||
}
|
||||
}()
|
||||
|
||||
if t.hostName == "" || t.roomName == "" {
|
||||
if err := t.parseInfo(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
body, err := http.Get(fmt.Sprintf(twitchTokenApiUrl, t.hostName), nil, twitchHeader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
token := gjson.GetBytes(body, "token").String()
|
||||
sig := gjson.GetBytes(body, "sig").String()
|
||||
p := fmt.Sprintf("%d", rand.Intn(9000000)+1000000)
|
||||
u, err := url.Parse(fmt.Sprintf(twitchLiveBaseUrl, t.hostName))
|
||||
v := &url.Values{}
|
||||
v.Add("allow_source", "true")
|
||||
v.Add("allow_audio_only", "true")
|
||||
v.Add("allow_spectre", "true")
|
||||
v.Add("p", p)
|
||||
v.Add("player", "twitchweb")
|
||||
v.Add("segment_preference", "4")
|
||||
v.Add("sig", sig)
|
||||
v.Add("token", token)
|
||||
u.RawQuery = v.Encode()
|
||||
return []*url.URL{u}, nil
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const twitchTestUrl = "https://www.twitch.tv/wuyikoei"
|
||||
|
||||
func TestTwitchLive_GetInfo(t *testing.T) {
|
||||
u, _ := url.Parse(twitchTestUrl)
|
||||
live, _ := NewLive(u)
|
||||
t.Log(live.GetInfo())
|
||||
}
|
||||
|
||||
func TestTwitchLive_GetStreamUrls(t *testing.T) {
|
||||
u, _ := url.Parse(twitchTestUrl)
|
||||
live, _ := NewLive(u)
|
||||
t.Log(live.GetStreamUrls())
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/hr3lxphr6j/bililive-go/src/lib/http"
|
||||
)
|
||||
|
||||
const yizhiboApiUrl = "http://www.yizhibo.com/live/h5api/get_basic_live_info"
|
||||
|
||||
type YiZhiBoLive struct {
|
||||
abstractLive
|
||||
}
|
||||
|
||||
func (y *YiZhiBoLive) requestRoomInfo() ([]byte, error) {
|
||||
scid := strings.Split(strings.Split(y.Url.Path, "/")[2], ".")[0]
|
||||
body, err := http.Get(yizhiboApiUrl, map[string]string{"scid": scid}, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if gjson.GetBytes(body, "result").Int() != 1 {
|
||||
return nil, &RoomNotExistsError{y.Url}
|
||||
} else {
|
||||
return body, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (y *YiZhiBoLive) GetInfo() (info *Info, err error) {
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
err = e.(error)
|
||||
}
|
||||
}()
|
||||
|
||||
data, err := y.requestRoomInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info = &Info{
|
||||
Live: y,
|
||||
HostName: gjson.GetBytes(data, "data.nickname").String(),
|
||||
RoomName: gjson.GetBytes(data, "data.live_title").String(),
|
||||
Status: gjson.GetBytes(data, "data.status").Int() == 10,
|
||||
}
|
||||
y.cachedInfo = info
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (y *YiZhiBoLive) GetStreamUrls() (us []*url.URL, err error) {
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
err = e.(error)
|
||||
}
|
||||
}()
|
||||
|
||||
data, err := y.requestRoomInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
u, err := url.Parse(gjson.GetBytes(data, "data.play_url").String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []*url.URL{u}, nil
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const yizhiboTestUrl = "https://www.yizhibo.com/l/ytbdVP1SSmWXzUx_.html"
|
||||
|
||||
func TestYiZhiBoLive_GetInfo(t *testing.T) {
|
||||
u, _ := url.Parse(yizhiboTestUrl)
|
||||
live, _ := NewLive(u)
|
||||
t.Log(live.GetInfo())
|
||||
}
|
||||
|
||||
func TestYiZhiBoLive_GetStreamUrls(t *testing.T) {
|
||||
u, _ := url.Parse(yizhiboTestUrl)
|
||||
live, _ := NewLive(u)
|
||||
t.Log(live.GetStreamUrls())
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/hr3lxphr6j/bililive-go/src/lib/http"
|
||||
)
|
||||
|
||||
const (
|
||||
zhanQiApiUrl = "https://www.zhanqi.tv/api/static/v2.1/room/domain/%s.json"
|
||||
)
|
||||
|
||||
type ZhanQiLive struct {
|
||||
abstractLive
|
||||
}
|
||||
|
||||
func (z *ZhanQiLive) requestRoomInfo() ([]byte, error) {
|
||||
body, err := http.Get(fmt.Sprintf(zhanQiApiUrl, strings.Split(z.Url.Path, "/")[1]), nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if gjson.GetBytes(body, "code").Int() == 0 {
|
||||
return body, nil
|
||||
} else {
|
||||
return nil, &RoomNotExistsError{z.Url}
|
||||
}
|
||||
}
|
||||
|
||||
func (z *ZhanQiLive) GetInfo() (info *Info, err error) {
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
err = e.(error)
|
||||
}
|
||||
}()
|
||||
|
||||
body, err := z.requestRoomInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info = &Info{
|
||||
Live: z,
|
||||
HostName: gjson.GetBytes(body, "data.nickname").String(),
|
||||
RoomName: gjson.GetBytes(body, "data.title").String(),
|
||||
Status: gjson.GetBytes(body, "data.status").String() == "4",
|
||||
}
|
||||
z.cachedInfo = info
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (z *ZhanQiLive) GetStreamUrls() (us []*url.URL, err error) {
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
err = e.(error)
|
||||
}
|
||||
}()
|
||||
|
||||
body, err := z.requestRoomInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
videoLevels := gjson.GetBytes(body, "data.flashvars.VideoLevels").String()
|
||||
data, err := base64.StdEncoding.DecodeString(videoLevels)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m3u8, _ := url.Parse(gjson.GetBytes(data, "streamUrl").String())
|
||||
return []*url.URL{m3u8}, nil
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const zhanQiTestUrl = "https://www.zhanqi.tv/12qaq"
|
||||
|
||||
func TestZhanQiLive_GetInfo(t *testing.T) {
|
||||
u, _ := url.Parse(zhanQiTestUrl)
|
||||
live, _ := NewLive(u)
|
||||
t.Log(live.GetInfo())
|
||||
}
|
||||
|
||||
func TestZhanQiLive_GetStreamUrls(t *testing.T) {
|
||||
u, _ := url.Parse(zhanQiTestUrl)
|
||||
live, _ := NewLive(u)
|
||||
t.Log(live.GetStreamUrls())
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/bluele/gcache"
|
||||
|
||||
_ "github.com/hr3lxphr6j/bililive-go/src/cmd/bililive/internal"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/cmd/bililive/internal/flag"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/configs"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/consts"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/instance"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/lib/events"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/lib/utils"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/listeners"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/live"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/log"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/recorders"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/servers"
|
||||
)
|
||||
|
||||
func init() {
|
||||
if !utils.IsFFmpegExist() {
|
||||
fmt.Fprintf(os.Stderr, "FFmpeg binary not found, Please Check.\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func getConfig() (*configs.Config, error) {
|
||||
var config *configs.Config
|
||||
if *flag.Conf != "" {
|
||||
c, err := configs.NewConfigWithFile(*flag.Conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config = c
|
||||
} else {
|
||||
config = flag.GenConfigFromFlags()
|
||||
}
|
||||
return config, config.Verify()
|
||||
}
|
||||
|
||||
func main() {
|
||||
config, err := getConfig()
|
||||
if err != nil {
|
||||
fmt.Fprint(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
inst := new(instance.Instance)
|
||||
inst.Config = config
|
||||
inst.Cache = gcache.New(128).LRU().Build()
|
||||
ctx := context.WithValue(context.Background(), instance.InstanceKey, inst)
|
||||
|
||||
logger := log.New(ctx)
|
||||
logger.Infof("%s Version: %s Link Start", consts.AppName, consts.AppVersion)
|
||||
logger.Debugf("%+v", consts.AppInfo)
|
||||
logger.Debugf("%+v", inst.Config)
|
||||
|
||||
events.NewDispatcher(ctx)
|
||||
|
||||
inst.Lives = make(map[live.ID]live.Live)
|
||||
for _, room := range inst.Config.LiveRooms {
|
||||
u, err := url.Parse(room)
|
||||
if err != nil {
|
||||
logger.WithField("url", room).Error(err)
|
||||
continue
|
||||
}
|
||||
l, err := live.New(u, inst.Cache)
|
||||
if err != nil {
|
||||
logger.WithField("url", room).Error(err.Error())
|
||||
continue
|
||||
}
|
||||
if _, ok := inst.Lives[l.GetLiveId()]; ok {
|
||||
logger.Errorf("%s is exist!", room)
|
||||
continue
|
||||
}
|
||||
inst.Lives[l.GetLiveId()] = l
|
||||
}
|
||||
|
||||
if inst.Config.RPC.Enable {
|
||||
if err := servers.NewServer(ctx).Start(ctx); err != nil {
|
||||
logger.WithError(err).Fatalf("failed to init server")
|
||||
}
|
||||
}
|
||||
lm := listeners.NewManager(ctx)
|
||||
rm := recorders.NewManager(ctx)
|
||||
if err := lm.Start(ctx); err != nil {
|
||||
logger.Fatalf("failed to init listener manager, error: %s", err)
|
||||
}
|
||||
if err := rm.Start(ctx); err != nil {
|
||||
logger.Fatalf("failed to init recorder manager, error: %s", err)
|
||||
}
|
||||
|
||||
for _, _live := range inst.Lives {
|
||||
if err := lm.AddListener(ctx, _live); err != nil {
|
||||
logger.WithFields(map[string]interface{}{"url": _live.GetRawUrl()}).Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
c := make(chan os.Signal)
|
||||
signal.Notify(c, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
|
||||
go func() {
|
||||
<-c
|
||||
if inst.Config.RPC.Enable {
|
||||
inst.Server.Close(ctx)
|
||||
}
|
||||
inst.ListenerManager.Close(ctx)
|
||||
inst.RecorderManager.Close(ctx)
|
||||
}()
|
||||
|
||||
inst.WaitGroup.Wait()
|
||||
logger.Info("Bye~")
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package flag
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/alecthomas/kingpin"
|
||||
|
||||
"github.com/hr3lxphr6j/bililive-go/src/configs"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/consts"
|
||||
)
|
||||
|
||||
var (
|
||||
App = kingpin.New(consts.AppName, "A command-line live stream save tools.").Version(consts.AppVersion)
|
||||
Debug = App.Flag("debug", "Enable debug mode.").Default("false").Bool()
|
||||
Interval = App.Flag("interval", "Interval of query live status").Default("20").Short('t').Int()
|
||||
Output = App.Flag("output", "Output file path.").Short('o').Default("./").String()
|
||||
Input = App.Flag("input", "Live room urls").Short('i').Strings()
|
||||
Conf = App.Flag("config", "Config file.").Short('c').String()
|
||||
Rpc = App.Flag("enable-rpc", "Enable RPC server.").Default("false").Bool()
|
||||
RpcAddr = App.Flag("rpc-addr", "RPC server listen port").Default(":8080").String()
|
||||
RpcToken = App.Flag("rpc-token", "RPC server token.").String()
|
||||
RpcTLS = App.Flag("enable-rpc-tls", "Enable TLS for RPC server").Bool()
|
||||
CertFile = App.Flag("rpc-tls-cert-file", "Cert file for TLS on RPC").String()
|
||||
KeyFile = App.Flag("rpc-tls-key-file", "Key file for TLS on RPC").String()
|
||||
NativeFlvParser = App.Flag("native-flv-parser", "use native flv parser").Default("false").Bool()
|
||||
)
|
||||
|
||||
func init() {
|
||||
kingpin.MustParse(App.Parse(os.Args[1:]))
|
||||
}
|
||||
|
||||
func GenConfigFromFlags() *configs.Config {
|
||||
return &configs.Config{
|
||||
RPC: configs.RPC{
|
||||
Enable: *Rpc,
|
||||
Bind: *RpcAddr,
|
||||
Token: *RpcToken,
|
||||
TLS: configs.TLS{
|
||||
Enable: *RpcTLS,
|
||||
CertFile: *CertFile,
|
||||
KeyFile: *KeyFile,
|
||||
},
|
||||
},
|
||||
Debug: *Debug,
|
||||
Interval: *Interval,
|
||||
OutPutPath: *Output,
|
||||
LiveRooms: *Input,
|
||||
Feature: configs.Feature{
|
||||
UseNativeFlvParser: *NativeFlvParser,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
_ "github.com/hr3lxphr6j/bililive-go/src/live/bilibili"
|
||||
_ "github.com/hr3lxphr6j/bililive-go/src/live/cc"
|
||||
_ "github.com/hr3lxphr6j/bililive-go/src/live/douyu"
|
||||
_ "github.com/hr3lxphr6j/bililive-go/src/live/huomao"
|
||||
_ "github.com/hr3lxphr6j/bililive-go/src/live/huya"
|
||||
_ "github.com/hr3lxphr6j/bililive-go/src/live/longzhu"
|
||||
_ "github.com/hr3lxphr6j/bililive-go/src/live/openrec"
|
||||
_ "github.com/hr3lxphr6j/bililive-go/src/live/qq"
|
||||
_ "github.com/hr3lxphr6j/bililive-go/src/live/twitch"
|
||||
_ "github.com/hr3lxphr6j/bililive-go/src/live/yizhibo"
|
||||
_ "github.com/hr3lxphr6j/bililive-go/src/live/zhanqi"
|
||||
)
|
||||
+52
-25
@@ -2,9 +2,9 @@ package configs
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
@@ -15,15 +15,47 @@ type TLS struct {
|
||||
CertFile string `yaml:"cert_file"`
|
||||
KeyFile string `yaml:"key_file"`
|
||||
}
|
||||
|
||||
func (t *TLS) Verify() error {
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
if !t.Enable {
|
||||
return nil
|
||||
}
|
||||
if _, err := tls.LoadX509KeyPair(t.CertFile, t.KeyFile); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type RPC struct {
|
||||
Enable bool `yaml:"enable"`
|
||||
Port string `yaml:"port"`
|
||||
Bind string `yaml:"port"`
|
||||
Token string `yaml:"token"`
|
||||
TLS TLS `yaml:"tls"`
|
||||
}
|
||||
|
||||
func (r *RPC) Verify() error {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
if !r.Enable {
|
||||
return nil
|
||||
}
|
||||
if _, err := net.ResolveTCPAddr("tcp", r.Bind); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.TLS.Verify(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Feature struct {
|
||||
UseNativeFlvParser bool `yaml:"use_native_flv_parser"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
RPC RPC `yaml:"rpc"`
|
||||
Debug bool `yaml:"debug"`
|
||||
@@ -34,44 +66,39 @@ type Config struct {
|
||||
file string
|
||||
}
|
||||
|
||||
func VerifyConfig(config *Config) error {
|
||||
if config.Interval <= 0 {
|
||||
return errors.New(fmt.Sprintf(`the interval can not <= 0`))
|
||||
func (c *Config) Verify() error {
|
||||
if c == nil {
|
||||
return fmt.Errorf("config is null")
|
||||
}
|
||||
if _, err := os.Stat(config.OutPutPath); err != nil {
|
||||
return errors.New(fmt.Sprintf(`the out put path: "%s" is not exist`, config.OutPutPath))
|
||||
if err := c.RPC.Verify(); err != nil {
|
||||
return err
|
||||
}
|
||||
if config.RPC.Enable {
|
||||
if config.RPC.Port == "" {
|
||||
return errors.New("rpc listen port can not be null")
|
||||
}
|
||||
if config.RPC.TLS.Enable {
|
||||
if _, err := tls.LoadX509KeyPair(config.RPC.TLS.CertFile, config.RPC.TLS.KeyFile); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if c.Interval <= 0 {
|
||||
return fmt.Errorf("the interval can not <= 0")
|
||||
}
|
||||
if _, err := os.Stat(c.OutPutPath); err != nil {
|
||||
return fmt.Errorf(`the out put path: "%s" is not exist`, c.OutPutPath)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewConfigWithFile(configFilePath string) (*Config, error) {
|
||||
b, err := ioutil.ReadFile(configFilePath)
|
||||
func NewConfigWithFile(file string) (*Config, error) {
|
||||
b, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
return nil, errors.New(fmt.Sprintf("can`t open file: %s", configFilePath))
|
||||
return nil, fmt.Errorf("can`t open file: %s", file)
|
||||
}
|
||||
config := new(Config)
|
||||
err = yaml.Unmarshal(b, config)
|
||||
if err != nil {
|
||||
if err = yaml.Unmarshal(b, config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.file = configFilePath
|
||||
config.file = file
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (config *Config) Marshal() error {
|
||||
b, err := yaml.Marshal(config)
|
||||
func (c *Config) Marshal() error {
|
||||
b, err := yaml.Marshal(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ioutil.WriteFile(config.file, b, os.ModeAppend)
|
||||
return ioutil.WriteFile(c.file, b, os.ModeAppend)
|
||||
}
|
||||
@@ -1,9 +1,50 @@
|
||||
package configs
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewConfig(t *testing.T) {
|
||||
t.Log(NewConfigWithFile("../../config.yml"))
|
||||
file := "../../config.yml"
|
||||
c, err := NewConfigWithFile("../../config.yml")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, file, c.file)
|
||||
|
||||
}
|
||||
|
||||
func TestTLS_Verify(t *testing.T) {
|
||||
var tls *TLS
|
||||
assert.NoError(t, tls.Verify())
|
||||
tls = new(TLS)
|
||||
assert.NoError(t, tls.Verify())
|
||||
tls.Enable = true
|
||||
assert.Error(t, tls.Verify())
|
||||
}
|
||||
|
||||
func TestRPC_Verify(t *testing.T) {
|
||||
var rpc *RPC
|
||||
assert.NoError(t, rpc.Verify())
|
||||
rpc = new(RPC)
|
||||
rpc.Bind = "foo@bar"
|
||||
assert.NoError(t, rpc.Verify())
|
||||
rpc.Enable = true
|
||||
assert.Error(t, rpc.Verify())
|
||||
}
|
||||
|
||||
func TestConfig_Verify(t *testing.T) {
|
||||
var cfg *Config
|
||||
assert.Error(t, cfg.Verify())
|
||||
cfg = &Config{
|
||||
Interval: 30,
|
||||
OutPutPath: os.TempDir(),
|
||||
}
|
||||
assert.NoError(t, cfg.Verify())
|
||||
cfg.Interval = 0
|
||||
assert.Error(t, cfg.Verify())
|
||||
cfg.Interval = 30
|
||||
cfg.OutPutPath = "foobar"
|
||||
assert.Error(t, cfg.Verify())
|
||||
}
|
||||
Executable
+56
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
set -o nounset
|
||||
|
||||
OUTPUT_PATH=bin
|
||||
SRC_PATH="${GOPATH}/src/github.com/hr3lxphr6j/bililive-go"
|
||||
CONSTS_PATH="github.com/hr3lxphr6j/bililive-go/src/consts"
|
||||
|
||||
_build() {
|
||||
target=$1
|
||||
bin_name=$2
|
||||
ld_flags=$3
|
||||
go build \
|
||||
-gcflags=${GOFLAGS:-""} \
|
||||
-o ${OUTPUT_PATH}/${bin_name} \
|
||||
-ldflags="${ld_flags}" \
|
||||
./src/cmd/${target}/
|
||||
}
|
||||
|
||||
build() {
|
||||
target=$1
|
||||
|
||||
if [[ ${target} == 'bililive' ]]; then
|
||||
now=$(date '+%Y-%m-%d_%H:%M:%S')
|
||||
rev=$(echo "${rev:-$(git rev-parse HEAD)}")
|
||||
ver=$(git describe --tags --always)
|
||||
ld_flags="-s -w -X ${CONSTS_PATH}.BuildTime=${now} -X ${CONSTS_PATH}.AppVersion=${ver} -X ${CONSTS_PATH}.GitHash=${rev}"
|
||||
fi
|
||||
|
||||
if [[ $(go env GOOS) == "windows" ]]; then
|
||||
ext=".exe"
|
||||
fi
|
||||
|
||||
bin_name="${target}-$(go env GOOS)-$(go env GOARCH)${ext:-}"
|
||||
|
||||
_build "${target}" "${bin_name}" "${ld_flags:-}"
|
||||
|
||||
if [[ ${UPX_ENABLE:-"0"} == "1" ]]; then
|
||||
upx --no-progress ${OUTPUT_PATH}/"${bin_name}"
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
pushd ${SRC_PATH} >/dev/null
|
||||
if [[ ! -d src/cmd/$1 ]]; then
|
||||
echo 'Target not exist in src/cmd/'
|
||||
popd >/dev/null
|
||||
exit 1
|
||||
fi
|
||||
build $1
|
||||
popd >/dev/null
|
||||
}
|
||||
|
||||
main $@
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
set -o nounset
|
||||
|
||||
make PLATFORM=linux ARCH=amd64 bililive
|
||||
docker build -t 'chigusa/bililive-go' .
|
||||
Executable
+54
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
set -o nounset
|
||||
|
||||
DISTS=(
|
||||
"darwin-amd64"
|
||||
"windows-amd64"
|
||||
"windows-386"
|
||||
"linux-amd64"
|
||||
"linux-386"
|
||||
"linux-arm64"
|
||||
"linux-arm"
|
||||
)
|
||||
BIN_PATH=bin
|
||||
|
||||
package() {
|
||||
pushd $BIN_PATH >/dev/null 2>&1
|
||||
file=$1
|
||||
type=$2
|
||||
case $type in
|
||||
zip)
|
||||
zip ${file%.exe}.zip ${file} ../config.yml
|
||||
;;
|
||||
tar)
|
||||
tar zcvf ${file}.tar.gz ${file} -C ../ config.yml
|
||||
;;
|
||||
7z)
|
||||
7z a ${file}.7z ${file} ../config.yml
|
||||
;;
|
||||
*) ;;
|
||||
|
||||
esac
|
||||
popd >/dev/null 2>&1
|
||||
}
|
||||
|
||||
for dist in ${DISTS[@]}; do
|
||||
platform=$(echo ${dist} | cut -d'-' -f1)
|
||||
arch=$(echo ${dist} | cut -d'-' -f2)
|
||||
make PLATFORM=${platform} ARCH=${arch} bililive
|
||||
done
|
||||
|
||||
for file in $(ls $BIN_PATH); do
|
||||
if [[ $file == *".tar.gz" || $file == *".zip" || $file == *".7z" || $file == *".yml" ]]; then
|
||||
continue
|
||||
fi
|
||||
package_type=tar
|
||||
if [[ $file == *"windows"* ]]; then
|
||||
package_type=zip
|
||||
fi
|
||||
package $file $package_type
|
||||
rm -f $BIN_PATH/$file
|
||||
done
|
||||
@@ -3,18 +3,21 @@ package instance
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/hr3lxphr6j/bililive-go/src/api"
|
||||
"github.com/bluele/gcache"
|
||||
|
||||
"github.com/hr3lxphr6j/bililive-go/src/configs"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/interfaces"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/live"
|
||||
)
|
||||
|
||||
type Instance struct {
|
||||
WaitGroup sync.WaitGroup // 用于阻塞主线程,直到所有模块均结束
|
||||
Config *configs.Config // 配置信息
|
||||
Logger *interfaces.Logger // Log
|
||||
Lives map[api.LiveId]api.Live // 直播间集合
|
||||
Server interfaces.Module // RPC服务
|
||||
EventDispatcher interfaces.Module // 事件分发
|
||||
ListenerManager interfaces.Module // 直播间状态监听
|
||||
RecorderManager interfaces.Module // 录制模块
|
||||
WaitGroup sync.WaitGroup
|
||||
Config *configs.Config
|
||||
Logger *interfaces.Logger
|
||||
Lives map[live.ID]live.Live
|
||||
Cache gcache.Cache
|
||||
Server interfaces.Module
|
||||
EventDispatcher interfaces.Module
|
||||
ListenerManager interfaces.Module
|
||||
RecorderManager interfaces.Module
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
//go:generate mockgen -package mock -destination mock/mock.go github.com/hr3lxphr6j/bililive-go/src/lib/events Dispatcher
|
||||
package events
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/hr3lxphr6j/bililive-go/src/instance"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/interfaces"
|
||||
)
|
||||
|
||||
func NewDispatcher(ctx context.Context) Dispatcher {
|
||||
ed := &dispatcher{
|
||||
saver: make(map[EventType]*list.List),
|
||||
}
|
||||
inst := instance.GetInstance(ctx)
|
||||
if inst != nil {
|
||||
inst.EventDispatcher = ed
|
||||
}
|
||||
return ed
|
||||
}
|
||||
|
||||
type Dispatcher interface {
|
||||
interfaces.Module
|
||||
AddEventListener(eventType EventType, listener *EventListener)
|
||||
RemoveEventListener(eventType EventType, listener *EventListener)
|
||||
RemoveAllEventListener(eventType EventType)
|
||||
DispatchEvent(event *Event)
|
||||
}
|
||||
|
||||
type dispatcher struct {
|
||||
sync.RWMutex
|
||||
saver map[EventType]*list.List // map<EventType, List<*EventListener>>
|
||||
}
|
||||
|
||||
func (e *dispatcher) Start(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *dispatcher) Close(ctx context.Context) {
|
||||
|
||||
}
|
||||
|
||||
func (e *dispatcher) AddEventListener(eventType EventType, listener *EventListener) {
|
||||
e.Lock()
|
||||
defer e.Unlock()
|
||||
listeners, ok := e.saver[eventType]
|
||||
if !ok || listener == nil {
|
||||
listeners = list.New()
|
||||
e.saver[eventType] = listeners
|
||||
}
|
||||
listeners.PushBack(listener)
|
||||
}
|
||||
|
||||
func (e *dispatcher) RemoveEventListener(eventType EventType, listener *EventListener) {
|
||||
e.Lock()
|
||||
defer e.Unlock()
|
||||
listeners, ok := e.saver[eventType]
|
||||
if !ok || listeners == nil {
|
||||
return
|
||||
}
|
||||
for e := listeners.Front(); e != nil; e = e.Next() {
|
||||
if e.Value == listener {
|
||||
listeners.Remove(e)
|
||||
}
|
||||
}
|
||||
if listeners.Len() == 0 {
|
||||
delete(e.saver, eventType)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *dispatcher) RemoveAllEventListener(eventType EventType) {
|
||||
e.Lock()
|
||||
defer e.Unlock()
|
||||
e.saver = make(map[EventType]*list.List)
|
||||
}
|
||||
|
||||
func (e *dispatcher) DispatchEvent(event *Event) {
|
||||
if event == nil {
|
||||
return
|
||||
}
|
||||
e.RLock()
|
||||
listeners, ok := e.saver[event.Type]
|
||||
if !ok || listeners == nil {
|
||||
e.RUnlock()
|
||||
return
|
||||
}
|
||||
hs := make([]*EventListener, 0)
|
||||
for e := listeners.Front(); e != nil; e = e.Next() {
|
||||
hs = append(hs, e.Value.(*EventListener))
|
||||
}
|
||||
e.RUnlock()
|
||||
go func() {
|
||||
for _, h := range hs {
|
||||
h.Handler(event)
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package events
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAddAndRemoveEventListener(t *testing.T) {
|
||||
d := NewDispatcher(context.Background()).(*dispatcher)
|
||||
l := NewEventListener(func(event *Event) {})
|
||||
d.AddEventListener("test", l)
|
||||
d.AddEventListener("test2", NewEventListener(func(event *Event) {}))
|
||||
ls, ok := d.saver["test"]
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, l, ls.Front().Value)
|
||||
d.RemoveEventListener("test", l)
|
||||
_, ok = d.saver["test"]
|
||||
assert.False(t, ok)
|
||||
d.RemoveAllEventListener("test2")
|
||||
assert.Empty(t, d.saver)
|
||||
}
|
||||
|
||||
func TestDispatchEvent(t *testing.T) {
|
||||
l := make([]int, 0)
|
||||
d := NewDispatcher(context.Background()).(*dispatcher)
|
||||
d.AddEventListener("test", NewEventListener(func(event *Event) {
|
||||
l = append(l, 0)
|
||||
}))
|
||||
d.AddEventListener("test", NewEventListener(func(event *Event) {
|
||||
l = append(l, 1)
|
||||
}))
|
||||
d.AddEventListener("test", NewEventListener(func(event *Event) {
|
||||
l = append(l, 2)
|
||||
}))
|
||||
d.AddEventListener("test", NewEventListener(func(event *Event) {
|
||||
l = append(l, 3)
|
||||
}))
|
||||
d.DispatchEvent(NewEvent("test", nil))
|
||||
time.Sleep(time.Second)
|
||||
assert.Equal(t, []int{0, 1, 2, 3}, l)
|
||||
}
|
||||
@@ -1,12 +1,9 @@
|
||||
package events
|
||||
|
||||
// 事件种类
|
||||
type EventType string
|
||||
|
||||
// 事件回调
|
||||
type EventHandler func(event *Event)
|
||||
|
||||
// 事件类型基类
|
||||
type Event struct {
|
||||
Type EventType
|
||||
Object interface{}
|
||||
@@ -16,7 +13,6 @@ func NewEvent(eventType EventType, object interface{}) *Event {
|
||||
return &Event{eventType, object}
|
||||
}
|
||||
|
||||
// 事件监听器
|
||||
type EventListener struct {
|
||||
Handler EventHandler
|
||||
}
|
||||
@@ -24,6 +20,3 @@ type EventListener struct {
|
||||
func NewEventListener(handler EventHandler) *EventListener {
|
||||
return &EventListener{handler}
|
||||
}
|
||||
|
||||
// 事件监听器集合
|
||||
type eventListenerSet map[*EventListener]bool
|
||||
@@ -1,58 +0,0 @@
|
||||
package events
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/hr3lxphr6j/bililive-go/src/instance"
|
||||
)
|
||||
|
||||
func NewIEventDispatcher(ctx context.Context) IEventDispatcher {
|
||||
ed := &EventDispatcher{
|
||||
saver: make(map[EventType]eventListenerSet),
|
||||
}
|
||||
instance.GetInstance(ctx).EventDispatcher = ed
|
||||
return ed
|
||||
}
|
||||
|
||||
// 事件分发器接口
|
||||
type IEventDispatcher interface {
|
||||
AddEventListener(eventType EventType, listener *EventListener)
|
||||
RemoveEventListener(eventType EventType, listener *EventListener)
|
||||
RemoveAllEventListener(eventType EventType)
|
||||
DispatchEvent(event *Event)
|
||||
}
|
||||
|
||||
// 事件分发器
|
||||
type EventDispatcher struct {
|
||||
saver map[EventType]eventListenerSet
|
||||
}
|
||||
|
||||
func (e *EventDispatcher) Start(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *EventDispatcher) Close(ctx context.Context) {
|
||||
|
||||
}
|
||||
|
||||
func (e *EventDispatcher) AddEventListener(eventType EventType, listener *EventListener) {
|
||||
_, isExist := e.saver[eventType]
|
||||
if !isExist {
|
||||
e.saver[eventType] = make(map[*EventListener]bool)
|
||||
}
|
||||
e.saver[eventType][listener] = true
|
||||
}
|
||||
|
||||
func (e *EventDispatcher) RemoveEventListener(eventType EventType, listener *EventListener) {
|
||||
delete(e.saver[eventType], listener)
|
||||
}
|
||||
|
||||
func (e *EventDispatcher) RemoveAllEventListener(eventType EventType) {
|
||||
delete(e.saver, eventType)
|
||||
}
|
||||
|
||||
func (e *EventDispatcher) DispatchEvent(event *Event) {
|
||||
for l := range e.saver[event.Type] {
|
||||
go l.Handler(event)
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package events
|
||||
|
||||
import "errors"
|
||||
|
||||
var listenerExistError = errors.New("this event listener was exist")
|
||||
var listenerNotExistError = errors.New("this event listener was not exist")
|
||||
|
||||
var eventExistError = errors.New("this event was exist")
|
||||
var eventNotExistError = errors.New("this event was not exist")
|
||||
@@ -0,0 +1,109 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/hr3lxphr6j/bililive-go/src/lib/events (interfaces: Dispatcher)
|
||||
|
||||
// Package mock is a generated GoMock package.
|
||||
package mock
|
||||
|
||||
import (
|
||||
context "context"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
events "github.com/hr3lxphr6j/bililive-go/src/lib/events"
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
// MockDispatcher is a mock of Dispatcher interface
|
||||
type MockDispatcher struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockDispatcherMockRecorder
|
||||
}
|
||||
|
||||
// MockDispatcherMockRecorder is the mock recorder for MockDispatcher
|
||||
type MockDispatcherMockRecorder struct {
|
||||
mock *MockDispatcher
|
||||
}
|
||||
|
||||
// NewMockDispatcher creates a new mock instance
|
||||
func NewMockDispatcher(ctrl *gomock.Controller) *MockDispatcher {
|
||||
mock := &MockDispatcher{ctrl: ctrl}
|
||||
mock.recorder = &MockDispatcherMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockDispatcher) EXPECT() *MockDispatcherMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// AddEventListener mocks base method
|
||||
func (m *MockDispatcher) AddEventListener(arg0 events.EventType, arg1 *events.EventListener) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "AddEventListener", arg0, arg1)
|
||||
}
|
||||
|
||||
// AddEventListener indicates an expected call of AddEventListener
|
||||
func (mr *MockDispatcherMockRecorder) AddEventListener(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddEventListener", reflect.TypeOf((*MockDispatcher)(nil).AddEventListener), arg0, arg1)
|
||||
}
|
||||
|
||||
// Close mocks base method
|
||||
func (m *MockDispatcher) Close(arg0 context.Context) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "Close", arg0)
|
||||
}
|
||||
|
||||
// Close indicates an expected call of Close
|
||||
func (mr *MockDispatcherMockRecorder) Close(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockDispatcher)(nil).Close), arg0)
|
||||
}
|
||||
|
||||
// DispatchEvent mocks base method
|
||||
func (m *MockDispatcher) DispatchEvent(arg0 *events.Event) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "DispatchEvent", arg0)
|
||||
}
|
||||
|
||||
// DispatchEvent indicates an expected call of DispatchEvent
|
||||
func (mr *MockDispatcherMockRecorder) DispatchEvent(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DispatchEvent", reflect.TypeOf((*MockDispatcher)(nil).DispatchEvent), arg0)
|
||||
}
|
||||
|
||||
// RemoveAllEventListener mocks base method
|
||||
func (m *MockDispatcher) RemoveAllEventListener(arg0 events.EventType) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "RemoveAllEventListener", arg0)
|
||||
}
|
||||
|
||||
// RemoveAllEventListener indicates an expected call of RemoveAllEventListener
|
||||
func (mr *MockDispatcherMockRecorder) RemoveAllEventListener(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveAllEventListener", reflect.TypeOf((*MockDispatcher)(nil).RemoveAllEventListener), arg0)
|
||||
}
|
||||
|
||||
// RemoveEventListener mocks base method
|
||||
func (m *MockDispatcher) RemoveEventListener(arg0 events.EventType, arg1 *events.EventListener) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "RemoveEventListener", arg0, arg1)
|
||||
}
|
||||
|
||||
// RemoveEventListener indicates an expected call of RemoveEventListener
|
||||
func (mr *MockDispatcherMockRecorder) RemoveEventListener(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveEventListener", reflect.TypeOf((*MockDispatcher)(nil).RemoveEventListener), arg0, arg1)
|
||||
}
|
||||
|
||||
// Start mocks base method
|
||||
func (m *MockDispatcher) Start(arg0 context.Context) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Start", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Start indicates an expected call of Start
|
||||
func (mr *MockDispatcherMockRecorder) Start(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockDispatcher)(nil).Start), arg0)
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
var commonHeader = map[string]string{
|
||||
"Accept": "application/json, text/javascript, */*; q=0.01",
|
||||
"Accept-Encoding": "gzip, deflate",
|
||||
"Accept-Language": "zh-CN,zh;q=0.8,en-US;q=0.6,en;q=0.4,zh-TW;q=0.2",
|
||||
"Connection": "keep-alive",
|
||||
"User-Agent": "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",
|
||||
}
|
||||
|
||||
var client = new(http.Client)
|
||||
|
||||
func Get(path string, header, query map[string]string) ([]byte, error) {
|
||||
return do(http.MethodGet, path, header, query, nil)
|
||||
}
|
||||
|
||||
func Post(path string, header, query map[string]string, body []byte) ([]byte, error) {
|
||||
return do(http.MethodPost, path, header, query, body)
|
||||
}
|
||||
|
||||
func parseResponse(resp *http.Response) ([]byte, error) {
|
||||
var reader io.ReadCloser
|
||||
defer func() {
|
||||
if reader != nil {
|
||||
reader.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
switch resp.Header.Get("Content-Encoding") {
|
||||
case "gzip":
|
||||
r, err := gzip.NewReader(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reader = r
|
||||
default:
|
||||
reader = resp.Body
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
func do(method string, path string, header, query map[string]string, body []byte) ([]byte, error) {
|
||||
var r io.Reader = nil
|
||||
if body != nil {
|
||||
r = bytes.NewReader(body)
|
||||
}
|
||||
req, err := http.NewRequest(method, path, r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if header == nil || len(header) == 0 {
|
||||
header = commonHeader
|
||||
}
|
||||
for k, v := range header {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
q := url.Values{}
|
||||
for k, v := range query {
|
||||
q.Set(k, v)
|
||||
}
|
||||
req.URL.RawQuery = q.Encode()
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, errors.New(resp.Status)
|
||||
}
|
||||
return parseResponse(resp)
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"github.com/mozillazg/request"
|
||||
)
|
||||
|
||||
var commonHeader = map[string]string{
|
||||
"Accept": "application/json, text/javascript, */*; q=0.01",
|
||||
"Accept-Encoding": "gzip, deflate",
|
||||
"Accept-Language": "zh-CN,zh;q=0.8,en-US;q=0.6,en;q=0.4,zh-TW;q=0.2",
|
||||
"Connection": "keep-alive",
|
||||
"User-Agent": "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",
|
||||
}
|
||||
|
||||
var client = new(http.Client)
|
||||
|
||||
func parseResponse(resp *request.Response) ([]byte, error) {
|
||||
var reader io.ReadCloser
|
||||
defer func() {
|
||||
if reader != nil {
|
||||
reader.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
switch resp.Header.Get("Content-Encoding") {
|
||||
case "gzip":
|
||||
r, err := gzip.NewReader(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reader = r
|
||||
default:
|
||||
reader = resp.Body
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
func Get(url string, query map[string]string, header map[string]string) ([]byte, error) {
|
||||
req := request.NewRequest(client)
|
||||
if header != nil {
|
||||
req.Headers = header
|
||||
} else {
|
||||
req.Headers = commonHeader
|
||||
}
|
||||
req.Params = query
|
||||
|
||||
if resp, err := req.Get(url); err == nil {
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, errors.New(resp.Status)
|
||||
}
|
||||
return parseResponse(resp)
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func Post(url string, query map[string]string, body []byte, header map[string]string) ([]byte, error) {
|
||||
req := request.NewRequest(client)
|
||||
if header != nil {
|
||||
req.Headers = header
|
||||
} else {
|
||||
req.Headers = commonHeader
|
||||
}
|
||||
req.Params = query
|
||||
req.Body = bytes.NewReader(body)
|
||||
|
||||
if resp, err := req.Post(url); err == nil {
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, errors.New(resp.Status)
|
||||
}
|
||||
return parseResponse(resp)
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGet(t *testing.T) {
|
||||
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, http.MethodGet, r.Method)
|
||||
assert.EqualValues(t, "bar", r.Header.Get("foo"))
|
||||
assert.Len(t, r.URL.Query(), 1)
|
||||
assert.EqualValues(t, "test", r.URL.Query().Get("q"))
|
||||
w.Write([]byte("OK"))
|
||||
}))
|
||||
b, err := Get(fmt.Sprintf("%s/test", s.URL), map[string]string{"foo": "bar"}, map[string]string{"q": "test"})
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, "OK", string(b))
|
||||
}
|
||||
|
||||
func TestPost(t *testing.T) {
|
||||
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, http.MethodPost, r.Method)
|
||||
assert.EqualValues(t, "bar", r.Header.Get("foo"))
|
||||
assert.Len(t, r.URL.Query(), 1)
|
||||
assert.EqualValues(t, "test", r.URL.Query().Get("q"))
|
||||
b, err := ioutil.ReadAll(r.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, "hello", string(b))
|
||||
w.Write([]byte("OK"))
|
||||
}))
|
||||
b, err := Post(fmt.Sprintf("%s/test", s.URL), map[string]string{"foo": "bar"}, map[string]string{"q": "test"}, []byte("hello"))
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, "OK", string(b))
|
||||
}
|
||||
|
||||
func TestGZipContent(t *testing.T) {
|
||||
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, http.MethodGet, r.Method)
|
||||
assert.EqualValues(t, "bar", r.Header.Get("foo"))
|
||||
assert.Len(t, r.URL.Query(), 1)
|
||||
assert.EqualValues(t, "test", r.URL.Query().Get("q"))
|
||||
|
||||
w.Header().Add("Content-Encoding", "gzip")
|
||||
gw := gzip.NewWriter(w)
|
||||
_, err := gw.Write([]byte("hello, gzip!"))
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, gw.Flush())
|
||||
assert.NoError(t, gw.Close())
|
||||
}))
|
||||
b, err := Get(fmt.Sprintf("%s/test", s.URL), map[string]string{"foo": "bar"}, map[string]string{"q": "test"})
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, "hello, gzip!", string(b))
|
||||
}
|
||||
@@ -5,30 +5,42 @@ import (
|
||||
"net/url"
|
||||
"os/exec"
|
||||
"sync"
|
||||
|
||||
"github.com/hr3lxphr6j/bililive-go/src/lib/parser"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/live"
|
||||
)
|
||||
|
||||
const (
|
||||
Name = "ffmpeg"
|
||||
|
||||
userAgent = "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"
|
||||
)
|
||||
|
||||
func init() {
|
||||
parser.Register(Name, new(builder))
|
||||
}
|
||||
|
||||
type builder struct{}
|
||||
|
||||
func (b *builder) Build() (parser.Parser, error) {
|
||||
return &Parser{
|
||||
closeOnce: new(sync.Once),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type Parser struct {
|
||||
cmd *exec.Cmd
|
||||
cmdStdIn io.WriteCloser
|
||||
closeOnce *sync.Once
|
||||
}
|
||||
|
||||
func New() *Parser {
|
||||
return &Parser{
|
||||
closeOnce: new(sync.Once),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Parser) ParseLiveStream(url *url.URL, file string) error {
|
||||
func (p *Parser) ParseLiveStream(url *url.URL, live live.Live, file string) error {
|
||||
p.cmd = exec.Command(
|
||||
"ffmpeg",
|
||||
"-loglevel", "warning",
|
||||
"-y", "-re",
|
||||
"-user_agent", userAgent,
|
||||
"-referer", live.GetRawUrl(),
|
||||
"-timeout", "60000000",
|
||||
"-i", url.String(),
|
||||
"-c", "copy",
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/hr3lxphr6j/bililive-go/src/lib/parser (interfaces: Parser)
|
||||
|
||||
// Package mock is a generated GoMock package.
|
||||
package mock
|
||||
|
||||
import (
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
live "github.com/hr3lxphr6j/bililive-go/src/live"
|
||||
url "net/url"
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
// MockParser is a mock of Parser interface
|
||||
type MockParser struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockParserMockRecorder
|
||||
}
|
||||
|
||||
// MockParserMockRecorder is the mock recorder for MockParser
|
||||
type MockParserMockRecorder struct {
|
||||
mock *MockParser
|
||||
}
|
||||
|
||||
// NewMockParser creates a new mock instance
|
||||
func NewMockParser(ctrl *gomock.Controller) *MockParser {
|
||||
mock := &MockParser{ctrl: ctrl}
|
||||
mock.recorder = &MockParserMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockParser) EXPECT() *MockParserMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// ParseLiveStream mocks base method
|
||||
func (m *MockParser) ParseLiveStream(arg0 *url.URL, arg1 live.Live, arg2 string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ParseLiveStream", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// ParseLiveStream indicates an expected call of ParseLiveStream
|
||||
func (mr *MockParserMockRecorder) ParseLiveStream(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ParseLiveStream", reflect.TypeOf((*MockParser)(nil).ParseLiveStream), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// Stop mocks base method
|
||||
func (m *MockParser) Stop() error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Stop")
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Stop indicates an expected call of Stop
|
||||
func (mr *MockParserMockRecorder) Stop() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stop", reflect.TypeOf((*MockParser)(nil).Stop))
|
||||
}
|
||||
@@ -10,21 +10,41 @@ import (
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/hr3lxphr6j/bililive-go/src/lib/parser"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/lib/reader"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/live"
|
||||
)
|
||||
|
||||
const (
|
||||
Name = "native"
|
||||
|
||||
audioTag uint8 = 8
|
||||
videoTag uint8 = 9
|
||||
scriptTag uint8 = 18
|
||||
)
|
||||
|
||||
var (
|
||||
flvSign = []byte{0x46, 0x4c, 0x56, 0x01} // flv version01
|
||||
NotFlvStream = errors.New("not flv stream")
|
||||
UnknownTag = errors.New("unknown tag")
|
||||
flvSign = []byte{0x46, 0x4c, 0x56, 0x01} // flv version01
|
||||
|
||||
ErrNotFlvStream = errors.New("not flv stream")
|
||||
ErrUnknownTag = errors.New("unknown tag")
|
||||
)
|
||||
|
||||
func init() {
|
||||
parser.Register(Name, new(builder))
|
||||
}
|
||||
|
||||
type builder struct{}
|
||||
|
||||
func (b *builder) Build() (parser.Parser, error) {
|
||||
return &Parser{
|
||||
Metadata: Metadata{},
|
||||
hc: &http.Client{},
|
||||
stopCh: make(chan struct{}),
|
||||
closeOnce: new(sync.Once),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type Metadata struct {
|
||||
HasVideo, HasAudio bool
|
||||
}
|
||||
@@ -42,22 +62,13 @@ type Parser struct {
|
||||
closeOnce *sync.Once
|
||||
}
|
||||
|
||||
func NewParser() *Parser {
|
||||
return &Parser{
|
||||
Metadata: Metadata{},
|
||||
hc: &http.Client{},
|
||||
stopCh: make(chan struct{}),
|
||||
closeOnce: new(sync.Once),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Parser) ParseLiveStream(url *url.URL, file string) error {
|
||||
func (p *Parser) ParseLiveStream(url *url.URL, live live.Live, file string) error {
|
||||
// init input
|
||||
req, err := http.NewRequest("GET", url.String(), nil)
|
||||
req.Header.Add("User-Agent", "Chrome/59.0.3071.115")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Add("User-Agent", "Chrome/59.0.3071.115")
|
||||
resp, err := p.hc.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -93,7 +104,7 @@ func (p *Parser) doParse() error {
|
||||
}
|
||||
// signature
|
||||
if !bytes.Equal(b[:4], flvSign) {
|
||||
return NotFlvStream
|
||||
return ErrNotFlvStream
|
||||
}
|
||||
// flag
|
||||
p.Metadata.HasVideo = uint8(b[4])&(1<<2) != 0
|
||||
@@ -101,7 +112,7 @@ func (p *Parser) doParse() error {
|
||||
|
||||
// offset must be 9
|
||||
if binary.BigEndian.Uint32(b[5:]) != 9 {
|
||||
return NotFlvStream
|
||||
return ErrNotFlvStream
|
||||
}
|
||||
|
||||
// write flv header
|
||||
|
||||
@@ -24,7 +24,7 @@ func (p *Parser) parseTag() error {
|
||||
case scriptTag:
|
||||
return p.parseScriptTag(length)
|
||||
default:
|
||||
return UnknownTag
|
||||
return ErrUnknownTag
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
package flv
|
||||
|
||||
type SoundFormat uint8
|
||||
type SoundRate uint8
|
||||
type SoundSize uint8
|
||||
type SoundType uint8
|
||||
type AACPacketType uint8
|
||||
type AudioTagHeader struct {
|
||||
SoundFormat SoundFormat
|
||||
SoundRate SoundRate
|
||||
SoundSize SoundSize
|
||||
SoundType SoundType
|
||||
AACPacketType AACPacketType
|
||||
}
|
||||
type (
|
||||
SoundFormat uint8
|
||||
SoundRate uint8
|
||||
SoundSize uint8
|
||||
SoundType uint8
|
||||
AACPacketType uint8
|
||||
|
||||
AudioTagHeader struct {
|
||||
SoundFormat SoundFormat
|
||||
SoundRate SoundRate
|
||||
SoundSize SoundSize
|
||||
SoundType SoundType
|
||||
AACPacketType AACPacketType
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
// SoundFormat
|
||||
|
||||
@@ -4,15 +4,18 @@ import (
|
||||
"io"
|
||||
)
|
||||
|
||||
type FrameType uint8
|
||||
type CodeID uint8
|
||||
type AVCPacketType uint8
|
||||
type VideoTagHeader struct {
|
||||
FrameType FrameType
|
||||
CodeID CodeID
|
||||
AVCPacketType AVCPacketType
|
||||
CompositionTime uint32
|
||||
}
|
||||
type (
|
||||
FrameType uint8
|
||||
CodeID uint8
|
||||
AVCPacketType uint8
|
||||
|
||||
VideoTagHeader struct {
|
||||
FrameType FrameType
|
||||
CodeID CodeID
|
||||
AVCPacketType AVCPacketType
|
||||
CompositionTime uint32
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
// Frame Type
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
package h264
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
)
|
||||
|
||||
type NaluType uint8
|
||||
|
||||
const (
|
||||
SLICE NaluType = 1
|
||||
DPA NaluType = 2
|
||||
DPB NaluType = 3
|
||||
DPC NaluType = 4
|
||||
IDR NaluType = 5
|
||||
SEI NaluType = 6
|
||||
SPS NaluType = 7
|
||||
PPS NaluType = 8
|
||||
AUD NaluType = 9
|
||||
EOSEQ NaluType = 10
|
||||
EOSTREAM NaluType = 11
|
||||
FILL NaluType = 12
|
||||
UNKNOWN NaluType = 255
|
||||
)
|
||||
|
||||
var (
|
||||
NALUStartCode = []byte{0x00, 0x00, 0x01}
|
||||
NALUStartCode2 = []byte{0x00, 0x00, 0x00, 0x01}
|
||||
UnknownNALUStartCode = errors.New("unknown NALU start code")
|
||||
)
|
||||
|
||||
type Parser struct {
|
||||
buf *bytes.Buffer
|
||||
buf1, buf2, buf3, buf4 []byte
|
||||
}
|
||||
|
||||
func NewParser() *Parser {
|
||||
return &Parser{
|
||||
buf: bytes.NewBuffer(make([]byte, 2<<10)),
|
||||
buf1: make([]byte, 1),
|
||||
buf2: make([]byte, 2),
|
||||
buf3: make([]byte, 3),
|
||||
buf4: make([]byte, 4),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Parser) parseNALUStartCode(i io.Reader) ([]byte, error) {
|
||||
if n, err := i.Read(p.buf3); err != nil || n != len(p.buf3) {
|
||||
if err == nil {
|
||||
err = io.EOF
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if !bytes.Equal(p.buf3, NALUStartCode) {
|
||||
if n, err := i.Read(p.buf1); err != nil || n != len(p.buf1) {
|
||||
if err == nil {
|
||||
err = io.EOF
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if !bytes.Equal(append(p.buf3, p.buf1...), NALUStartCode2) {
|
||||
return nil, UnknownNALUStartCode
|
||||
} else {
|
||||
return NALUStartCode2, nil
|
||||
}
|
||||
} else {
|
||||
return NALUStartCode, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Parser) ParseAVCSequenceHeader(i io.Reader, length uint32) {
|
||||
|
||||
}
|
||||
|
||||
func (p *Parser) ParseAnnexBNalu(i io.Reader, length uint32) (NaluType, error) {
|
||||
startCode, err := p.parseNALUStartCode(i)
|
||||
if err != nil {
|
||||
return UNKNOWN, err
|
||||
}
|
||||
if n, err := i.Read(p.buf1); err != nil || n != len(p.buf1) {
|
||||
if err == nil {
|
||||
err = io.EOF
|
||||
}
|
||||
return UNKNOWN, err
|
||||
}
|
||||
naluType := NaluType(uint8(p.buf1[0]) & 31)
|
||||
buf := make([]byte, length-1-uint32(len(startCode)))
|
||||
if n, err := i.Read(buf); err != nil || n != len(buf) {
|
||||
if err == nil {
|
||||
err = io.EOF
|
||||
}
|
||||
return UNKNOWN, err
|
||||
}
|
||||
return naluType, nil
|
||||
}
|
||||
|
||||
func (p *Parser) ParseAVCCNalu(i io.Reader, o io.Writer, length uint32) {
|
||||
|
||||
}
|
||||
@@ -1,8 +1,32 @@
|
||||
//go:generate mockgen -package mock -destination mock/mock.go github.com/hr3lxphr6j/bililive-go/src/lib/parser Parser
|
||||
package parser
|
||||
|
||||
import "net/url"
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
|
||||
"github.com/hr3lxphr6j/bililive-go/src/live"
|
||||
)
|
||||
|
||||
type Builder interface {
|
||||
Build() (Parser, error)
|
||||
}
|
||||
|
||||
type Parser interface {
|
||||
ParseLiveStream(url *url.URL, file string) error
|
||||
ParseLiveStream(url *url.URL, live live.Live, file string) error
|
||||
Stop() error
|
||||
}
|
||||
|
||||
var m = make(map[string]Builder)
|
||||
|
||||
func Register(name string, b Builder) {
|
||||
m[name] = b
|
||||
}
|
||||
|
||||
func New(name string) (Parser, error) {
|
||||
builder, ok := m[name]
|
||||
if !ok {
|
||||
return nil, errors.New("unknown parser")
|
||||
}
|
||||
return builder.Build()
|
||||
}
|
||||
@@ -11,8 +11,9 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
OutOfBuffer = errors.New("n is bigger than len of buffer")
|
||||
pool = sync.Pool{New: func() interface{} { return make([]byte, defaultBufferSize) }}
|
||||
ErrOutOfBuffer = errors.New("n is bigger than len of buffer")
|
||||
|
||||
pool = sync.Pool{New: func() interface{} { return make([]byte, defaultBufferSize) }}
|
||||
)
|
||||
|
||||
type BufferedReader struct {
|
||||
@@ -30,7 +31,7 @@ func New(rd io.Reader) *BufferedReader {
|
||||
|
||||
func (b *BufferedReader) ReadN(n int) ([]byte, error) {
|
||||
if n > len(b.buf)-b.r {
|
||||
return nil, OutOfBuffer
|
||||
return nil, ErrOutOfBuffer
|
||||
}
|
||||
b.l = b.r
|
||||
return b.readN(n, b.l)
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html"
|
||||
"regexp"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type StringFilter interface {
|
||||
Do(string) string
|
||||
}
|
||||
|
||||
type StringFilterFunc func(string) string
|
||||
|
||||
func (f StringFilterFunc) Do(s string) string {
|
||||
return f(s)
|
||||
}
|
||||
|
||||
type StringFilterChain struct {
|
||||
filters []StringFilter
|
||||
}
|
||||
|
||||
func NewStringFilterChain(filter ...StringFilter) *StringFilterChain {
|
||||
return &StringFilterChain{
|
||||
filters: filter,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *StringFilterChain) Do(str string) string {
|
||||
for _, f := range c.filters {
|
||||
str = f.Do(str)
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
func ParseString(str string, filter ...StringFilter) string {
|
||||
return NewStringFilterChain(filter...).Do(str)
|
||||
}
|
||||
|
||||
var ParseUnicode = StringFilterFunc(func(str string) string {
|
||||
buf := new(bytes.Buffer)
|
||||
chars := []byte(str)
|
||||
for i := 0; i < len(str); {
|
||||
if chars[i] == 92 && chars[i+1] == 117 {
|
||||
t, _ := strconv.ParseInt(string(chars[i+2:i+6]), 16, 32)
|
||||
buf.WriteString(fmt.Sprintf("%c", t))
|
||||
i += 6
|
||||
} else {
|
||||
buf.WriteByte(chars[i])
|
||||
i++
|
||||
}
|
||||
}
|
||||
return buf.String()
|
||||
})
|
||||
|
||||
var ReplaceIllegalChar = StringFilterFunc(func(str string) string {
|
||||
reg := regexp.MustCompile(`[\/\\\:\*\?\"\<\>\|]`)
|
||||
return reg.ReplaceAllString(str, "_")
|
||||
})
|
||||
|
||||
var UnescapeHTMLEntity = StringFilterFunc(html.UnescapeString)
|
||||
+25
-24
@@ -1,14 +1,12 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/url"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func IsFFmpegExist() bool {
|
||||
@@ -22,27 +20,6 @@ func GetMd5String(b []byte) string {
|
||||
return hex.EncodeToString(md5Obj.Sum(nil))
|
||||
}
|
||||
|
||||
func ParseUnicode(str string) string {
|
||||
buf := new(bytes.Buffer)
|
||||
chars := []byte(str)
|
||||
for i := 0; i < len(str); {
|
||||
if chars[i] == 92 && chars[i+1] == 117 {
|
||||
t, _ := strconv.ParseInt(string(chars[i+2:i+6]), 16, 32)
|
||||
buf.WriteString(fmt.Sprintf("%c", t))
|
||||
i += 6
|
||||
} else {
|
||||
buf.WriteByte(chars[i])
|
||||
i++
|
||||
}
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func ReplaceIllegalChar(str string) string {
|
||||
reg := regexp.MustCompile(`[\/\\\:\*\?\"\<\>\|]`)
|
||||
return reg.ReplaceAllString(str, "_")
|
||||
}
|
||||
|
||||
var (
|
||||
lowercaseRunes = []rune("abcdefghijklmnopqrstuvwxyz")
|
||||
uppercaseRunes = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
@@ -59,3 +36,27 @@ func GenRandomName(n int) string {
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func Match1(re, str string) string {
|
||||
reg, err := regexp.Compile(re)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
match := reg.FindStringSubmatch(str)
|
||||
if match == nil || len(match) < 2 {
|
||||
return ""
|
||||
}
|
||||
return match[1]
|
||||
}
|
||||
|
||||
func GenUrls(strs ...string) ([]*url.URL, error) {
|
||||
urls := make([]*url.URL, 0, len(strs))
|
||||
for _, str := range strs {
|
||||
u, err := url.Parse(str)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
urls = append(urls, u)
|
||||
}
|
||||
return urls, nil
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package utils
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestReplaceIllegalChar(t *testing.T) {
|
||||
t.Log(ReplaceIllegalChar(":123.mp4"))
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package listeners
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrListenerExist = errors.New("this live has a listener")
|
||||
ErrListenerNotExist = errors.New("this live has not a listener")
|
||||
)
|
||||
File renamed without changes.
+36
-17
@@ -1,3 +1,4 @@
|
||||
//go:generate mockgen -package listeners -destination mock_test.go github.com/hr3lxphr6j/bililive-go/src/listeners Listener,Manager
|
||||
package listeners
|
||||
|
||||
import (
|
||||
@@ -5,11 +6,11 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/hr3lxphr6j/bililive-go/src/api"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/configs"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/instance"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/interfaces"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/lib/events"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/live"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -19,73 +20,91 @@ const (
|
||||
stopped
|
||||
)
|
||||
|
||||
func NewListener(ctx context.Context, live api.Live) *Listener {
|
||||
type Listener interface {
|
||||
Start() error
|
||||
Close()
|
||||
}
|
||||
|
||||
func NewListener(ctx context.Context, live live.Live) Listener {
|
||||
inst := instance.GetInstance(ctx)
|
||||
return &Listener{
|
||||
return &listener{
|
||||
Live: live,
|
||||
status: false,
|
||||
config: inst.Config,
|
||||
stop: make(chan struct{}),
|
||||
ed: inst.EventDispatcher.(events.IEventDispatcher),
|
||||
ed: inst.EventDispatcher.(events.Dispatcher),
|
||||
logger: inst.Logger,
|
||||
state: begin,
|
||||
}
|
||||
}
|
||||
|
||||
type Listener struct {
|
||||
Live api.Live
|
||||
type listener struct {
|
||||
Live live.Live
|
||||
status bool
|
||||
|
||||
config *configs.Config
|
||||
ed events.IEventDispatcher
|
||||
ed events.Dispatcher
|
||||
logger *interfaces.Logger
|
||||
|
||||
state uint32
|
||||
stop chan struct{}
|
||||
}
|
||||
|
||||
func (l *Listener) Start() error {
|
||||
func (l *listener) Start() error {
|
||||
if !atomic.CompareAndSwapUint32(&l.state, begin, pending) {
|
||||
return nil
|
||||
}
|
||||
defer atomic.CompareAndSwapUint32(&l.state, pending, running)
|
||||
|
||||
l.logger.WithFields(l.Live.GetInfoMap()).Info("Listener Start")
|
||||
l.ed.DispatchEvent(events.NewEvent(ListenStart, l.Live))
|
||||
l.refresh()
|
||||
go l.run()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Listener) Close() {
|
||||
func (l *listener) Close() {
|
||||
if !atomic.CompareAndSwapUint32(&l.state, running, stopped) {
|
||||
return
|
||||
}
|
||||
l.logger.WithFields(l.Live.GetInfoMap()).Info("Listener Close")
|
||||
l.ed.DispatchEvent(events.NewEvent(ListenStop, l.Live))
|
||||
close(l.stop)
|
||||
}
|
||||
|
||||
func (l *Listener) refresh() {
|
||||
func (l *listener) refresh() {
|
||||
info, err := l.Live.GetInfo()
|
||||
if err != nil {
|
||||
l.logger.
|
||||
WithError(err).
|
||||
WithField("url", l.Live.GetRawUrl()).
|
||||
Error("failed to load room info")
|
||||
return
|
||||
}
|
||||
if info.Status == l.status {
|
||||
return
|
||||
}
|
||||
l.status = info.Status
|
||||
|
||||
var (
|
||||
evtTyp events.EventType
|
||||
logInfo string
|
||||
fields = map[string]interface{}{
|
||||
"room": info.RoomName,
|
||||
"host": info.HostName,
|
||||
}
|
||||
)
|
||||
if l.status {
|
||||
l.Live.SetLastStartTime(time.Now())
|
||||
l.logger.WithFields(l.Live.GetInfoMap()).Info("Live Start")
|
||||
l.ed.DispatchEvent(events.NewEvent(LiveStart, l.Live))
|
||||
evtTyp = LiveStart
|
||||
logInfo = "Live Start"
|
||||
} else {
|
||||
l.logger.WithFields(l.Live.GetInfoMap()).Info("Live End")
|
||||
l.ed.DispatchEvent(events.NewEvent(LiveEnd, l.Live))
|
||||
evtTyp = LiveEnd
|
||||
logInfo = "Live end"
|
||||
}
|
||||
l.ed.DispatchEvent(events.NewEvent(evtTyp, l.Live))
|
||||
l.logger.WithFields(fields).Info(logInfo)
|
||||
}
|
||||
|
||||
func (l *Listener) run() {
|
||||
func (l *listener) run() {
|
||||
ticker := time.NewTicker(time.Duration(l.config.Interval) * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
package listeners
|
||||
|
||||
import "errors"
|
||||
|
||||
var roomNotExistError = errors.New("live room was not exist")
|
||||
var listenerExistError = errors.New("this live has a listener")
|
||||
var listenerNotExistError = errors.New("this live has not a listener")
|
||||
@@ -1,95 +0,0 @@
|
||||
package listeners
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/hr3lxphr6j/bililive-go/src/api"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/instance"
|
||||
)
|
||||
|
||||
func NewIListenerManager(ctx context.Context) IListenerManager {
|
||||
lm := &ListenerManager{
|
||||
savers: make(map[api.LiveId]*Listener),
|
||||
lock: new(sync.RWMutex),
|
||||
}
|
||||
instance.GetInstance(ctx).ListenerManager = lm
|
||||
return lm
|
||||
}
|
||||
|
||||
// 监听管理器接口
|
||||
type IListenerManager interface {
|
||||
AddListener(ctx context.Context, live api.Live) error
|
||||
RemoveListener(ctx context.Context, liveId api.LiveId) error
|
||||
GetListener(ctx context.Context, liveId api.LiveId) (*Listener, error)
|
||||
HasListener(ctx context.Context, liveId api.LiveId) bool
|
||||
}
|
||||
|
||||
type ListenerManager struct {
|
||||
savers map[api.LiveId]*Listener
|
||||
lock *sync.RWMutex
|
||||
}
|
||||
|
||||
func (l *ListenerManager) Start(ctx context.Context) error {
|
||||
inst := instance.GetInstance(ctx)
|
||||
if inst.Config.RPC.Enable || len(inst.Lives) > 0 {
|
||||
inst.WaitGroup.Add(1)
|
||||
}
|
||||
instance.GetInstance(ctx).Logger.Info("ListenerManager Start")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *ListenerManager) Close(ctx context.Context) {
|
||||
l.lock.Lock()
|
||||
defer l.lock.Unlock()
|
||||
for id, listener := range l.savers {
|
||||
listener.Close()
|
||||
delete(l.savers, id)
|
||||
}
|
||||
inst := instance.GetInstance(ctx)
|
||||
inst.WaitGroup.Done()
|
||||
instance.GetInstance(ctx).Logger.Info("ListenerManager Closed")
|
||||
}
|
||||
|
||||
func (l *ListenerManager) AddListener(ctx context.Context, live api.Live) error {
|
||||
l.lock.Lock()
|
||||
defer l.lock.Unlock()
|
||||
|
||||
if _, ok := l.savers[live.GetLiveId()]; ok {
|
||||
return listenerExistError
|
||||
}
|
||||
listener := NewListener(ctx, live)
|
||||
listener.Start()
|
||||
l.savers[live.GetLiveId()] = listener
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *ListenerManager) RemoveListener(ctx context.Context, liveId api.LiveId) error {
|
||||
l.lock.Lock()
|
||||
defer l.lock.Unlock()
|
||||
|
||||
if listener, ok := l.savers[liveId]; !ok {
|
||||
return listenerNotExistError
|
||||
} else {
|
||||
listener.Close()
|
||||
delete(l.savers, liveId)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (l *ListenerManager) GetListener(ctx context.Context, liveId api.LiveId) (*Listener, error) {
|
||||
l.lock.RLock()
|
||||
defer l.lock.RUnlock()
|
||||
if r, ok := l.savers[liveId]; !ok {
|
||||
return nil, listenerNotExistError
|
||||
} else {
|
||||
return r, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (l *ListenerManager) HasListener(ctx context.Context, liveId api.LiveId) bool {
|
||||
l.lock.RLock()
|
||||
defer l.lock.RUnlock()
|
||||
_, ok := l.savers[liveId]
|
||||
return ok
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package listeners
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/bluele/gcache"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/hr3lxphr6j/bililive-go/src/configs"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/instance"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/lib/events"
|
||||
evtmock "github.com/hr3lxphr6j/bililive-go/src/lib/events/mock"
|
||||
livepkg "github.com/hr3lxphr6j/bililive-go/src/live"
|
||||
livemock "github.com/hr3lxphr6j/bililive-go/src/live/mock"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/log"
|
||||
)
|
||||
|
||||
func TestRefresh(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ed := evtmock.NewMockDispatcher(ctrl)
|
||||
ctx := context.WithValue(context.Background(), instance.InstanceKey, &instance.Instance{
|
||||
EventDispatcher: ed,
|
||||
Config: new(configs.Config),
|
||||
})
|
||||
log.New(ctx)
|
||||
live := livemock.NewMockLive(ctrl)
|
||||
l := NewListener(ctx, live).(*listener)
|
||||
|
||||
// false -> false
|
||||
live.EXPECT().GetInfo().Return(&livepkg.Info{Status: false}, nil)
|
||||
l.refresh()
|
||||
assert.False(t, l.status)
|
||||
|
||||
// false -> true
|
||||
live.EXPECT().GetInfo().Return(&livepkg.Info{Status: true}, nil)
|
||||
live.EXPECT().SetLastStartTime(gomock.Any())
|
||||
ed.EXPECT().DispatchEvent(events.NewEvent(LiveStart, live))
|
||||
l.refresh()
|
||||
assert.True(t, l.status)
|
||||
|
||||
// true -> false
|
||||
live.EXPECT().GetInfo().Return(&livepkg.Info{Status: false}, nil)
|
||||
ed.EXPECT().DispatchEvent(events.NewEvent(LiveEnd, live))
|
||||
l.refresh()
|
||||
assert.False(t, l.status)
|
||||
}
|
||||
|
||||
func TestRefreshWithError(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ed := evtmock.NewMockDispatcher(ctrl)
|
||||
cache := gcache.New(4).LRU().Build()
|
||||
ctx := context.WithValue(context.Background(), instance.InstanceKey, &instance.Instance{
|
||||
EventDispatcher: ed,
|
||||
Cache: cache,
|
||||
Config: new(configs.Config),
|
||||
})
|
||||
log.New(ctx)
|
||||
live := livemock.NewMockLive(ctrl)
|
||||
l := NewListener(ctx, live).(*listener)
|
||||
|
||||
live.EXPECT().GetInfo().Return(nil, errors.New("this is error"))
|
||||
live.EXPECT().GetRawUrl().Return("")
|
||||
l.refresh()
|
||||
assert.False(t, l.status)
|
||||
}
|
||||
|
||||
func TestListenerStartAndClose(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ed := evtmock.NewMockDispatcher(ctrl)
|
||||
cache := gcache.New(4).LRU().Build()
|
||||
ctx := context.WithValue(context.Background(), instance.InstanceKey, &instance.Instance{
|
||||
EventDispatcher: ed,
|
||||
Cache: cache,
|
||||
Config: &configs.Config{Interval: 5},
|
||||
})
|
||||
log.New(ctx)
|
||||
live := livemock.NewMockLive(ctrl)
|
||||
live.EXPECT().GetInfo().Return(&livepkg.Info{Status: false}, nil)
|
||||
ed.EXPECT().DispatchEvent(gomock.Any()).Times(2)
|
||||
l := NewListener(ctx, live)
|
||||
assert.NoError(t, l.Start())
|
||||
assert.NoError(t, l.Start())
|
||||
l.Close()
|
||||
l.Close()
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package listeners
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/hr3lxphr6j/bililive-go/src/instance"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/interfaces"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/live"
|
||||
)
|
||||
|
||||
// for test
|
||||
var newListener = NewListener
|
||||
|
||||
func NewManager(ctx context.Context) Manager {
|
||||
lm := &manager{
|
||||
savers: make(map[live.ID]Listener),
|
||||
}
|
||||
instance.GetInstance(ctx).ListenerManager = lm
|
||||
return lm
|
||||
}
|
||||
|
||||
type Manager interface {
|
||||
interfaces.Module
|
||||
AddListener(ctx context.Context, live live.Live) error
|
||||
RemoveListener(ctx context.Context, liveId live.ID) error
|
||||
GetListener(ctx context.Context, liveId live.ID) (Listener, error)
|
||||
HasListener(ctx context.Context, liveId live.ID) bool
|
||||
}
|
||||
|
||||
type manager struct {
|
||||
lock sync.RWMutex
|
||||
savers map[live.ID]Listener
|
||||
}
|
||||
|
||||
func (m *manager) Start(ctx context.Context) error {
|
||||
inst := instance.GetInstance(ctx)
|
||||
if inst.Config.RPC.Enable || len(inst.Lives) > 0 {
|
||||
inst.WaitGroup.Add(1)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *manager) Close(ctx context.Context) {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
for id, listener := range m.savers {
|
||||
listener.Close()
|
||||
delete(m.savers, id)
|
||||
}
|
||||
inst := instance.GetInstance(ctx)
|
||||
inst.WaitGroup.Done()
|
||||
}
|
||||
|
||||
func (m *manager) AddListener(ctx context.Context, live live.Live) error {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
|
||||
if _, ok := m.savers[live.GetLiveId()]; ok {
|
||||
return ErrListenerExist
|
||||
}
|
||||
listener := newListener(ctx, live)
|
||||
m.savers[live.GetLiveId()] = listener
|
||||
return listener.Start()
|
||||
}
|
||||
|
||||
func (m *manager) RemoveListener(ctx context.Context, liveId live.ID) error {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
listener, ok := m.savers[liveId]
|
||||
if !ok {
|
||||
return ErrListenerNotExist
|
||||
}
|
||||
listener.Close()
|
||||
delete(m.savers, liveId)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *manager) GetListener(ctx context.Context, liveId live.ID) (Listener, error) {
|
||||
m.lock.RLock()
|
||||
defer m.lock.RUnlock()
|
||||
listener, ok := m.savers[liveId]
|
||||
if !ok {
|
||||
return nil, ErrListenerNotExist
|
||||
}
|
||||
return listener, nil
|
||||
}
|
||||
|
||||
func (m *manager) HasListener(ctx context.Context, liveId live.ID) bool {
|
||||
m.lock.RLock()
|
||||
defer m.lock.RUnlock()
|
||||
_, ok := m.savers[liveId]
|
||||
return ok
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package listeners
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/hr3lxphr6j/bililive-go/src/configs"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/instance"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/live"
|
||||
livemock "github.com/hr3lxphr6j/bililive-go/src/live/mock"
|
||||
)
|
||||
|
||||
func TestManagerAddAndRemoveListener(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
ctx := context.WithValue(context.Background(), instance.InstanceKey, &instance.Instance{})
|
||||
m := NewManager(ctx)
|
||||
backup := newListener
|
||||
newListener = func(ctx context.Context, live live.Live) Listener {
|
||||
ln := NewMockListener(ctrl)
|
||||
ln.EXPECT().Start().Return(nil)
|
||||
ln.EXPECT().Close()
|
||||
return ln
|
||||
}
|
||||
defer func() { newListener = backup }()
|
||||
l := livemock.NewMockLive(ctrl)
|
||||
l.EXPECT().GetLiveId().Return(live.ID("test")).Times(3)
|
||||
assert.NoError(t, m.AddListener(context.Background(), l))
|
||||
assert.Equal(t, ErrListenerExist, m.AddListener(context.Background(), l))
|
||||
ln, err := m.GetListener(context.Background(), "test")
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, ln)
|
||||
assert.True(t, m.HasListener(context.Background(), "test"))
|
||||
assert.NoError(t, m.RemoveListener(context.Background(), "test"))
|
||||
assert.Equal(t, ErrListenerNotExist, m.RemoveListener(context.Background(), "test"))
|
||||
_, err = m.GetListener(context.Background(), "test")
|
||||
assert.Equal(t, ErrListenerNotExist, err)
|
||||
assert.False(t, m.HasListener(context.Background(), "test"))
|
||||
}
|
||||
|
||||
func TestManagerStartAndClose(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.WithValue(context.Background(), instance.InstanceKey, &instance.Instance{
|
||||
Config: &configs.Config{
|
||||
RPC: configs.RPC{Enable: true},
|
||||
},
|
||||
})
|
||||
backup := newListener
|
||||
newListener = func(ctx context.Context, live live.Live) Listener {
|
||||
ln := NewMockListener(ctrl)
|
||||
ln.EXPECT().Start().Return(nil)
|
||||
ln.EXPECT().Close()
|
||||
return ln
|
||||
}
|
||||
defer func() { newListener = backup }()
|
||||
m := NewManager(ctx)
|
||||
assert.NoError(t, m.Start(ctx))
|
||||
for i := 0; i < 3; i++ {
|
||||
l := livemock.NewMockLive(ctrl)
|
||||
id := live.ID(fmt.Sprintf("test_%d", i))
|
||||
l.EXPECT().GetLiveId().Return(id).AnyTimes()
|
||||
assert.NoError(t, m.AddListener(ctx, l))
|
||||
}
|
||||
m.Close(ctx)
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/hr3lxphr6j/bililive-go/src/listeners (interfaces: Listener,Manager)
|
||||
|
||||
// Package listeners is a generated GoMock package.
|
||||
package listeners
|
||||
|
||||
import (
|
||||
context "context"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
live "github.com/hr3lxphr6j/bililive-go/src/live"
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
// MockListener is a mock of Listener interface
|
||||
type MockListener struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockListenerMockRecorder
|
||||
}
|
||||
|
||||
// MockListenerMockRecorder is the mock recorder for MockListener
|
||||
type MockListenerMockRecorder struct {
|
||||
mock *MockListener
|
||||
}
|
||||
|
||||
// NewMockListener creates a new mock instance
|
||||
func NewMockListener(ctrl *gomock.Controller) *MockListener {
|
||||
mock := &MockListener{ctrl: ctrl}
|
||||
mock.recorder = &MockListenerMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockListener) EXPECT() *MockListenerMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Close mocks base method
|
||||
func (m *MockListener) Close() {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "Close")
|
||||
}
|
||||
|
||||
// Close indicates an expected call of Close
|
||||
func (mr *MockListenerMockRecorder) Close() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockListener)(nil).Close))
|
||||
}
|
||||
|
||||
// Start mocks base method
|
||||
func (m *MockListener) Start() error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Start")
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Start indicates an expected call of Start
|
||||
func (mr *MockListenerMockRecorder) Start() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockListener)(nil).Start))
|
||||
}
|
||||
|
||||
// MockManager is a mock of Manager interface
|
||||
type MockManager struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockManagerMockRecorder
|
||||
}
|
||||
|
||||
// MockManagerMockRecorder is the mock recorder for MockManager
|
||||
type MockManagerMockRecorder struct {
|
||||
mock *MockManager
|
||||
}
|
||||
|
||||
// NewMockManager creates a new mock instance
|
||||
func NewMockManager(ctrl *gomock.Controller) *MockManager {
|
||||
mock := &MockManager{ctrl: ctrl}
|
||||
mock.recorder = &MockManagerMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockManager) EXPECT() *MockManagerMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// AddListener mocks base method
|
||||
func (m *MockManager) AddListener(arg0 context.Context, arg1 live.Live) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "AddListener", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// AddListener indicates an expected call of AddListener
|
||||
func (mr *MockManagerMockRecorder) AddListener(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddListener", reflect.TypeOf((*MockManager)(nil).AddListener), arg0, arg1)
|
||||
}
|
||||
|
||||
// Close mocks base method
|
||||
func (m *MockManager) Close(arg0 context.Context) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "Close", arg0)
|
||||
}
|
||||
|
||||
// Close indicates an expected call of Close
|
||||
func (mr *MockManagerMockRecorder) Close(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockManager)(nil).Close), arg0)
|
||||
}
|
||||
|
||||
// GetListener mocks base method
|
||||
func (m *MockManager) GetListener(arg0 context.Context, arg1 live.ID) (Listener, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetListener", arg0, arg1)
|
||||
ret0, _ := ret[0].(Listener)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetListener indicates an expected call of GetListener
|
||||
func (mr *MockManagerMockRecorder) GetListener(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetListener", reflect.TypeOf((*MockManager)(nil).GetListener), arg0, arg1)
|
||||
}
|
||||
|
||||
// HasListener mocks base method
|
||||
func (m *MockManager) HasListener(arg0 context.Context, arg1 live.ID) bool {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "HasListener", arg0, arg1)
|
||||
ret0, _ := ret[0].(bool)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// HasListener indicates an expected call of HasListener
|
||||
func (mr *MockManagerMockRecorder) HasListener(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasListener", reflect.TypeOf((*MockManager)(nil).HasListener), arg0, arg1)
|
||||
}
|
||||
|
||||
// RemoveListener mocks base method
|
||||
func (m *MockManager) RemoveListener(arg0 context.Context, arg1 live.ID) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "RemoveListener", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// RemoveListener indicates an expected call of RemoveListener
|
||||
func (mr *MockManagerMockRecorder) RemoveListener(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveListener", reflect.TypeOf((*MockManager)(nil).RemoveListener), arg0, arg1)
|
||||
}
|
||||
|
||||
// Start mocks base method
|
||||
func (m *MockManager) Start(arg0 context.Context) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Start", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Start indicates an expected call of Start
|
||||
func (mr *MockManagerMockRecorder) Start(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockManager)(nil).Start), arg0)
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package bilibili
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/hr3lxphr6j/bililive-go/src/lib/http"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/lib/utils"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/live"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/live/internal"
|
||||
)
|
||||
|
||||
const (
|
||||
domain = "live.bilibili.com"
|
||||
cnName = "哔哩哔哩"
|
||||
|
||||
roomInitUrl = "https://api.live.bilibili.com/room/v1/Room/room_init"
|
||||
roomApiUrl = "https://api.live.bilibili.com/room/v1/Room/get_info"
|
||||
userApiUrl = "https://api.live.bilibili.com/live_user/v1/UserInfo/get_anchor_in_room"
|
||||
liveApiUrl = "https://api.live.bilibili.com/room/v1/Room/playUrl"
|
||||
)
|
||||
|
||||
func init() {
|
||||
live.Register(domain, new(builder))
|
||||
}
|
||||
|
||||
type builder struct{}
|
||||
|
||||
func (b *builder) Build(url *url.URL) (live.Live, error) {
|
||||
return &Live{
|
||||
BaseLive: internal.NewBaseLive(url),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type Live struct {
|
||||
internal.BaseLive
|
||||
realID string
|
||||
}
|
||||
|
||||
func (l *Live) parseRealId() error {
|
||||
paths := strings.Split(l.Url.Path, "/")
|
||||
if len(paths) < 2 {
|
||||
return live.ErrRoomUrlIncorrect
|
||||
}
|
||||
body, err := http.Get(roomInitUrl, nil, map[string]string{
|
||||
"id": paths[1],
|
||||
})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if gjson.GetBytes(body, "code").Int() != 0 {
|
||||
return live.ErrRoomNotExist
|
||||
}
|
||||
l.realID = gjson.GetBytes(body, "data.room_id").String()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Live) GetInfo() (info *live.Info, err error) {
|
||||
// Parse the short id from URL to full id
|
||||
if l.realID == "" {
|
||||
if err := l.parseRealId(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
body, err := http.Get(roomApiUrl, nil, map[string]string{
|
||||
"room_id": l.realID,
|
||||
"from": "room",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if gjson.GetBytes(body, "code").Int() != 0 {
|
||||
return nil, live.ErrRoomNotExist
|
||||
}
|
||||
|
||||
info = &live.Info{
|
||||
Live: l,
|
||||
RoomName: gjson.GetBytes(body, "data.title").String(),
|
||||
Status: gjson.GetBytes(body, "data.live_status").Int() == 1,
|
||||
}
|
||||
|
||||
body2, err := http.Get(userApiUrl, nil, map[string]string{
|
||||
"roomid": l.realID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
info.HostName = gjson.GetBytes(body2, "data.info.uname").String()
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (l *Live) GetStreamUrls() (us []*url.URL, err error) {
|
||||
if l.realID == "" {
|
||||
if err := l.parseRealId(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
body, err := http.Get(liveApiUrl, nil, map[string]string{
|
||||
"cid": l.realID,
|
||||
"quality": "4",
|
||||
"platform": "web",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
urls := make([]string, 0, 0)
|
||||
gjson.GetBytes(body, "data.durl.#.url").ForEach(func(key, value gjson.Result) bool {
|
||||
urls = append(urls, value.String())
|
||||
return true
|
||||
})
|
||||
return utils.GenUrls(urls...)
|
||||
}
|
||||
|
||||
func (l *Live) GetPlatformCNName() string {
|
||||
return cnName
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package cc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"regexp"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/hr3lxphr6j/bililive-go/src/lib/http"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/lib/utils"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/live"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/live/internal"
|
||||
)
|
||||
|
||||
const (
|
||||
domain = "cc.163.com"
|
||||
cnName = "CC直播"
|
||||
|
||||
apiUrl = "http://cgi.v.cc.163.com/video_play_url/"
|
||||
)
|
||||
|
||||
func init() {
|
||||
live.Register(domain, new(builder))
|
||||
}
|
||||
|
||||
type builder struct{}
|
||||
|
||||
func (b *builder) Build(url *url.URL) (live.Live, error) {
|
||||
return &Live{
|
||||
BaseLive: internal.NewBaseLive(url),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type Live struct {
|
||||
internal.BaseLive
|
||||
ccid string
|
||||
}
|
||||
|
||||
func (l *Live) parseCCId() error {
|
||||
dom, err := http.Get(l.Url.String(), nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ccid := utils.Match1(`anchorCcId\s*:\s*'(\d*)'`, string(dom))
|
||||
if ccid == "" {
|
||||
return live.ErrInternalError
|
||||
}
|
||||
l.ccid = ccid
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Live) GetInfo() (info *live.Info, err error) {
|
||||
dom, err := http.Get(l.Url.String(), nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var (
|
||||
hostName = utils.UnescapeHTMLEntity(utils.Match1(`anchorName\s*:\s*'([^']*)',`, string(dom)))
|
||||
roomName = utils.UnescapeHTMLEntity(utils.Match1(`js-live-title nick" title\s*=\s*"([^"]*)"`, string(dom)))
|
||||
)
|
||||
|
||||
if hostName == "" || roomName == "" {
|
||||
return nil, live.ErrInternalError
|
||||
}
|
||||
|
||||
info = &live.Info{
|
||||
Live: l,
|
||||
HostName: hostName,
|
||||
RoomName: roomName,
|
||||
Status: len(regexp.MustCompile(`isLive\s*:\s*\d+,`).FindAll(dom, -1)) > 0,
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (l *Live) GetStreamUrls() (us []*url.URL, err error) {
|
||||
if l.ccid == "" {
|
||||
if err := l.parseCCId(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
data, err := http.Get(fmt.Sprintf("%s%s", apiUrl, l.ccid), nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return utils.GenUrls(
|
||||
gjson.GetBytes(data, "videourl").String(),
|
||||
gjson.GetBytes(data, "bakvideourl").String(),
|
||||
)
|
||||
}
|
||||
|
||||
func (l *Live) GetPlatformCNName() string {
|
||||
return cnName
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package api
|
||||
package douyu
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -10,6 +10,9 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/hr3lxphr6j/bililive-go/src/lib/utils"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/live"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/live/internal"
|
||||
|
||||
"github.com/robertkrimen/otto"
|
||||
"github.com/satori/go.uuid"
|
||||
"github.com/tidwall/gjson"
|
||||
@@ -23,11 +26,26 @@ import (
|
||||
Thanks
|
||||
*/
|
||||
const (
|
||||
douyuLiveInfoUrl = "https://open.douyucdn.cn/api/RoomApi/room"
|
||||
douyuLiveEncUrl = "https://www.douyu.com/swf_api/homeH5Enc"
|
||||
douyuLiveAPIUrl = "https://www.douyu.com/lapi/live/getH5Play"
|
||||
domain = "www.douyu.com"
|
||||
cnName = "斗鱼"
|
||||
|
||||
liveInfoUrl = "https://open.douyucdn.cn/api/RoomApi/room"
|
||||
liveEncUrl = "https://www.douyu.com/swf_api/homeH5Enc"
|
||||
liveAPIUrl = "https://www.douyu.com/lapi/live/getH5Play"
|
||||
)
|
||||
|
||||
func init() {
|
||||
live.Register(domain, new(builder))
|
||||
}
|
||||
|
||||
type builder struct{}
|
||||
|
||||
func (b *builder) Build(url *url.URL) (live.Live, error) {
|
||||
return &Live{
|
||||
BaseLive: internal.NewBaseLive(url),
|
||||
}, nil
|
||||
}
|
||||
|
||||
var (
|
||||
cryptoJS []byte
|
||||
header = map[string]string{
|
||||
@@ -80,9 +98,9 @@ var (
|
||||
`))
|
||||
)
|
||||
|
||||
func render(template *template.Template, data interface{}) (string, error) {
|
||||
func render(tmpl *template.Template, data interface{}) (string, error) {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
if err := template.Execute(buf, data); err != nil {
|
||||
if err := tmpl.Execute(buf, data); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return buf.String(), nil
|
||||
@@ -108,77 +126,70 @@ func getEngineWithCryptoJS() (*otto.Otto, error) {
|
||||
return engine, nil
|
||||
}
|
||||
|
||||
type DouyuLive struct {
|
||||
abstractLive
|
||||
type Live struct {
|
||||
internal.BaseLive
|
||||
roomID string
|
||||
}
|
||||
|
||||
func (d *DouyuLive) getRoomID() {
|
||||
if d.roomID != "" {
|
||||
func (l *Live) fetchRoomID() {
|
||||
if l.roomID != "" {
|
||||
return
|
||||
}
|
||||
d.roomID = strings.Split(d.Url.Path, "/")[1]
|
||||
body, err := http.Get(d.Url.String(), nil, nil)
|
||||
l.roomID = strings.Split(l.Url.Path, "/")[1]
|
||||
body, err := http.Get(l.Url.String(), nil, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, reg := range douyuRoomIDRegs {
|
||||
strs := reg.FindStringSubmatch(string(body))
|
||||
if len(strs) == 2 {
|
||||
d.roomID = strs[1]
|
||||
l.roomID = strs[1]
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DouyuLive) GetInfo() (info *Info, err error) {
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
err = e.(error)
|
||||
}
|
||||
}()
|
||||
d.getRoomID()
|
||||
body, err := http.Get(fmt.Sprintf("%s/%s", douyuLiveInfoUrl, d.roomID), nil, nil)
|
||||
func (l *Live) GetInfo() (info *live.Info, err error) {
|
||||
l.fetchRoomID()
|
||||
body, err := http.Get(fmt.Sprintf("%s/%s", liveInfoUrl, l.roomID), nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if gjson.GetBytes(body, "error").Int() != 0 {
|
||||
return nil, &RoomNotExistsError{d.Url}
|
||||
return nil, live.ErrRoomNotExist
|
||||
}
|
||||
info = &Info{
|
||||
Live: d,
|
||||
info = &live.Info{
|
||||
Live: l,
|
||||
HostName: gjson.GetBytes(body, "data.owner_name").String(),
|
||||
RoomName: gjson.GetBytes(body, "data.room_name").String(),
|
||||
Status: gjson.GetBytes(body, "data.room_status").String() == "1",
|
||||
}
|
||||
d.cachedInfo = info
|
||||
return info, nil
|
||||
|
||||
}
|
||||
|
||||
type context struct {
|
||||
DebugMessages string
|
||||
DecryptedCodes string
|
||||
Resoult string
|
||||
Ub98484234 string
|
||||
Workflow string
|
||||
}
|
||||
|
||||
func (d *DouyuLive) getSignParams() (url.Values, error) {
|
||||
body, err := http.Get(douyuLiveEncUrl, map[string]string{
|
||||
"rids": d.roomID,
|
||||
}, nil)
|
||||
func (l *Live) getSignParams() (url.Values, error) {
|
||||
body, err := http.Get(liveEncUrl, nil, map[string]string{
|
||||
"rids": l.roomID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
jsEnc := gjson.GetBytes(body, fmt.Sprintf("data.room%s", d.roomID)).String()
|
||||
fmt.Print(workflowReg.FindStringSubmatch(jsEnc))
|
||||
|
||||
jsEnc := gjson.GetBytes(body, fmt.Sprintf("data.room%s", l.roomID)).String()
|
||||
|
||||
workflow := ""
|
||||
workflowMatch := workflowReg.FindStringSubmatch(jsEnc)
|
||||
if len(workflowMatch) == 2 {
|
||||
if workflowMatch := workflowReg.FindStringSubmatch(jsEnc); len(workflowMatch) == 2 {
|
||||
workflow = workflowMatch[1]
|
||||
}
|
||||
context := context{
|
||||
|
||||
context := struct {
|
||||
DebugMessages string
|
||||
DecryptedCodes string
|
||||
Resoult string
|
||||
Ub98484234 string
|
||||
Workflow string
|
||||
}{
|
||||
DebugMessages: utils.GenRandomName(8),
|
||||
DecryptedCodes: utils.GenRandomName(8),
|
||||
Resoult: utils.GenRandomName(8),
|
||||
@@ -199,7 +210,6 @@ func (d *DouyuLive) getSignParams() (url.Values, error) {
|
||||
}
|
||||
|
||||
jsEnc = strings.ReplaceAll(jsEnc, fmt.Sprintf("eval(%s);", context.Workflow), jsPatch)
|
||||
|
||||
engine, err := getEngineWithCryptoJS()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -215,7 +225,7 @@ func (d *DouyuLive) getSignParams() (url.Values, error) {
|
||||
}
|
||||
did := strings.ReplaceAll(uuid.Must(uuid.NewV4()).String(), "-", "")
|
||||
ts := time.Now()
|
||||
res, err := engine.Call("ub98484234", nil, d.roomID, did, ts.Unix())
|
||||
res, err := engine.Call("ub98484234", nil, l.roomID, did, ts.Unix())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -239,19 +249,24 @@ func (d *DouyuLive) getSignParams() (url.Values, error) {
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func (d *DouyuLive) GetStreamUrls() (us []*url.URL, err error) {
|
||||
d.getRoomID()
|
||||
params, err := d.getSignParams()
|
||||
func (l *Live) GetStreamUrls() (us []*url.URL, err error) {
|
||||
l.fetchRoomID()
|
||||
params, err := l.getSignParams()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := http.Post(fmt.Sprintf("%s/%s", douyuLiveAPIUrl, d.roomID), nil, []byte(params.Encode()), header)
|
||||
resp, err := http.Post(fmt.Sprintf("%s/%s", liveAPIUrl, l.roomID), header, nil, []byte(params.Encode()))
|
||||
if gjson.GetBytes(resp, "error").Int() != 0 {
|
||||
return nil, fmt.Errorf("get stream error")
|
||||
}
|
||||
u, err := url.Parse(gjson.GetBytes(resp, "data.rtmp_url").String() + "/" + gjson.GetBytes(resp, "data.rtmp_live").String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []*url.URL{u}, nil
|
||||
return utils.GenUrls(
|
||||
fmt.Sprintf("%s/%s",
|
||||
gjson.GetBytes(resp, "data.rtmp_url").String(),
|
||||
gjson.GetBytes(resp, "data.rtmp_live").String(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func (l *Live) GetPlatformCNName() string {
|
||||
return cnName
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package live
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrRoomNotExist = errors.New("room not exists")
|
||||
ErrRoomUrlIncorrect = errors.New("room url incorrect")
|
||||
ErrInternalError = errors.New("internal error")
|
||||
)
|
||||
@@ -0,0 +1,118 @@
|
||||
package huomao
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/hr3lxphr6j/bililive-go/src/lib/http"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/lib/utils"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/live"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/live/internal"
|
||||
)
|
||||
|
||||
const (
|
||||
domain = "www.huomao.com"
|
||||
cnName = "火猫"
|
||||
|
||||
liveApiUrl = "http://www.huomao.com/swf/live_data"
|
||||
salt = "6FE26D855E1AEAE090E243EB1AF73685"
|
||||
)
|
||||
|
||||
func init() {
|
||||
live.Register(domain, new(builder))
|
||||
}
|
||||
|
||||
type builder struct{}
|
||||
|
||||
func (b *builder) Build(url *url.URL) (live.Live, error) {
|
||||
return &Live{
|
||||
BaseLive: internal.NewBaseLive(url),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type Live struct {
|
||||
internal.BaseLive
|
||||
isDuanbo bool
|
||||
}
|
||||
|
||||
func (l *Live) GetInfo() (info *live.Info, err error) {
|
||||
dom, err := http.Get(l.Url.String(), nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l.isDuanbo = utils.Match1(`face_label\s?=\s?(\d*);`, string(dom)) == "1"
|
||||
var (
|
||||
hostNameReg string
|
||||
roomNameReg string
|
||||
statusReg string
|
||||
)
|
||||
if l.isDuanbo {
|
||||
hostNameReg = `live_yz_h_nickName\s?=\s?"([^"]*)";`
|
||||
roomNameReg = `live_yz_h_channelName\s?=\s?"([^"]*)";`
|
||||
statusReg = `is_live\s?=\s?"?(\d*)"?;`
|
||||
} else {
|
||||
hostNameReg = `"nickname":"([^"]*)",`
|
||||
roomNameReg = `"channel":"([^"]*)"`
|
||||
statusReg = `"is_live":"?(\d*)"?,`
|
||||
}
|
||||
var (
|
||||
hostName = utils.Match1(hostNameReg, string(dom))
|
||||
roomName = utils.Match1(roomNameReg, string(dom))
|
||||
status = utils.Match1(statusReg, string(dom))
|
||||
)
|
||||
if hostName == "" || roomName == "" || status == "" {
|
||||
return nil, live.ErrInternalError
|
||||
}
|
||||
info = &live.Info{
|
||||
Live: l,
|
||||
HostName: hostName,
|
||||
RoomName: roomName,
|
||||
Status: status == "1",
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (l *Live) GetStreamUrls() ([]*url.URL, error) {
|
||||
dom, err := http.Get(l.Url.String(), nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var streamReg string
|
||||
if !l.isDuanbo {
|
||||
streamReg = `"stream":"([^"]*)"`
|
||||
} else {
|
||||
streamReg = `getFlash\("\d*","([^"]*)","\d*"\);`
|
||||
}
|
||||
var (
|
||||
from = "huomaoh5room"
|
||||
t = fmt.Sprintf("%d", time.Now().Unix())
|
||||
streamID = utils.Match1(streamReg, string(dom))
|
||||
token = utils.GetMd5String([]byte(fmt.Sprintf("%s%s%s%s", streamID, from, t, salt)))
|
||||
)
|
||||
body, err := http.Post(liveApiUrl, nil, map[string]string{
|
||||
"VideoIDS": streamID,
|
||||
"streamtype": "live",
|
||||
"cdns": "1",
|
||||
"from": from,
|
||||
"time": t,
|
||||
"token": token,
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
urls := make([]string, 0, 0)
|
||||
gjson.GetBytes(body, "streamList.#.list.#.url").ForEach(func(key, value gjson.Result) bool {
|
||||
for _, u := range value.Array() {
|
||||
urls = append(urls, u.String())
|
||||
}
|
||||
return true
|
||||
})
|
||||
return utils.GenUrls(urls...)
|
||||
}
|
||||
|
||||
func (l *Live) GetPlatformCNName() string {
|
||||
return cnName
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package huya
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hr3lxphr6j/bililive-go/src/lib/http"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/lib/utils"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/live"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/live/internal"
|
||||
)
|
||||
|
||||
const (
|
||||
domain = "www.huya.com"
|
||||
cnName = "虎牙"
|
||||
)
|
||||
|
||||
func init() {
|
||||
live.Register(domain, new(builder))
|
||||
}
|
||||
|
||||
type builder struct{}
|
||||
|
||||
func (b *builder) Build(url *url.URL) (live.Live, error) {
|
||||
return &Live{
|
||||
BaseLive: internal.NewBaseLive(url),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type Live struct {
|
||||
internal.BaseLive
|
||||
}
|
||||
|
||||
func (l *Live) GetInfo() (info *live.Info, err error) {
|
||||
dom, err := http.Get(l.Url.String(), nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if res := utils.Match1("哎呀,虎牙君找不到这个主播,要不搜索看看?", string(dom)); res != "" {
|
||||
return nil, live.ErrRoomNotExist
|
||||
}
|
||||
|
||||
var (
|
||||
strFilter = utils.NewStringFilterChain(utils.ParseUnicode, utils.UnescapeHTMLEntity)
|
||||
hostName = strFilter.Do(utils.Match1(`"nick":"([^"]*)"`, string(dom)))
|
||||
roomName = strFilter.Do(utils.Match1(`"introduction":"([^"]*)"`, string(dom)))
|
||||
status = strFilter.Do(utils.Match1(`"isOn":([^,]*),`, string(dom)))
|
||||
)
|
||||
|
||||
if hostName == "" || roomName == "" || status == "" {
|
||||
return nil, live.ErrInternalError
|
||||
}
|
||||
|
||||
info = &live.Info{
|
||||
Live: l,
|
||||
HostName: hostName,
|
||||
RoomName: roomName,
|
||||
Status: status == "true",
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (l *Live) GetStreamUrls() (us []*url.URL, err error) {
|
||||
dom, err := http.Get(l.Url.String(), nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var (
|
||||
sStreamName = utils.Match1(`"sStreamName":"([^"]*)"`, string(dom))
|
||||
sFlvUrl = strings.ReplaceAll(utils.Match1(`"sFlvUrl":"([^"]*)"`, string(dom)), `\/`, `/`)
|
||||
sFlvAntiCode = utils.Match1(`"sFlvAntiCode":"([^"]*)"`, string(dom))
|
||||
iLineIndex = utils.Match1(`"iLineIndex":(\d*),`, string(dom))
|
||||
uid = (time.Now().Unix()%1e7*1e6 + int64(1e3*rand.Float64())) % 4294967295
|
||||
)
|
||||
u, err := url.Parse(fmt.Sprintf("%s/%s.flv", sFlvUrl, sStreamName))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
value := url.Values{}
|
||||
value.Add("line", iLineIndex)
|
||||
value.Add("p2p", "0")
|
||||
value.Add("type", "web")
|
||||
value.Add("ver", "1805071653")
|
||||
value.Add("uid", fmt.Sprintf("%d", uid))
|
||||
u.RawQuery = fmt.Sprintf("%s&%s", value.Encode(), utils.UnescapeHTMLEntity(sFlvAntiCode))
|
||||
return []*url.URL{u}, nil
|
||||
}
|
||||
|
||||
func (l *Live) GetPlatformCNName() string {
|
||||
return cnName
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package live
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
type Info struct {
|
||||
Live Live
|
||||
HostName, RoomName string
|
||||
Status, Listening, Recoding bool
|
||||
}
|
||||
|
||||
func (i *Info) MarshalJSON() ([]byte, error) {
|
||||
t := struct {
|
||||
Id ID `json:"id"`
|
||||
LiveUrl string `json:"live_url"`
|
||||
PlatformCNName string `json:"platform_cn_name"`
|
||||
HostName string `json:"host_name"`
|
||||
RoomName string `json:"room_name"`
|
||||
Status bool `json:"status"`
|
||||
Listening bool `json:"listening"`
|
||||
Recoding bool `json:"recoding"`
|
||||
LastStartTime string `json:"last_start_time,omitempty"`
|
||||
LastStartTimeUnix int64 `json:"last_start_time_unix,omitempty"`
|
||||
}{
|
||||
Id: i.Live.GetLiveId(),
|
||||
LiveUrl: i.Live.GetRawUrl(),
|
||||
PlatformCNName: i.Live.GetPlatformCNName(),
|
||||
HostName: i.HostName,
|
||||
RoomName: i.RoomName,
|
||||
Status: i.Status,
|
||||
Listening: i.Listening,
|
||||
Recoding: i.Recoding,
|
||||
}
|
||||
if !i.Live.GetLastStartTime().IsZero() {
|
||||
t.LastStartTime = i.Live.GetLastStartTime().Format("2006-01-02 15:04:05")
|
||||
t.LastStartTimeUnix = i.Live.GetLastStartTime().Unix()
|
||||
}
|
||||
return json.Marshal(t)
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/hr3lxphr6j/bililive-go/src/lib/utils"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/live"
|
||||
)
|
||||
|
||||
type BaseLive struct {
|
||||
Url *url.URL
|
||||
LastStartTime time.Time
|
||||
LiveId live.ID
|
||||
}
|
||||
|
||||
func genLiveId(url *url.URL) live.ID {
|
||||
return live.ID(utils.GetMd5String([]byte(fmt.Sprintf("%s%s", url.Host, url.Path))))
|
||||
}
|
||||
|
||||
func NewBaseLive(url *url.URL) BaseLive {
|
||||
return BaseLive{
|
||||
Url: url,
|
||||
LiveId: genLiveId(url),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *BaseLive) GetLiveId() live.ID {
|
||||
return a.LiveId
|
||||
}
|
||||
|
||||
func (a *BaseLive) GetRawUrl() string {
|
||||
return a.Url.String()
|
||||
}
|
||||
|
||||
func (a *BaseLive) GetLastStartTime() time.Time {
|
||||
return a.LastStartTime
|
||||
}
|
||||
|
||||
func (a *BaseLive) SetLastStartTime(time time.Time) {
|
||||
a.LastStartTime = time
|
||||
}
|
||||
Executable
+81
@@ -0,0 +1,81 @@
|
||||
//go:generate mockgen -package mock -destination mock/mock.go github.com/hr3lxphr6j/bililive-go/src/live Live
|
||||
package live
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/bluele/gcache"
|
||||
)
|
||||
|
||||
var (
|
||||
m = make(map[string]Builder)
|
||||
)
|
||||
|
||||
func Register(domain string, b Builder) {
|
||||
m[domain] = b
|
||||
}
|
||||
|
||||
func getBuilder(domain string) (Builder, bool) {
|
||||
builder, ok := m[domain]
|
||||
return builder, ok
|
||||
}
|
||||
|
||||
type Builder interface {
|
||||
Build(*url.URL) (Live, error)
|
||||
}
|
||||
|
||||
type ID string
|
||||
|
||||
type Live interface {
|
||||
GetLiveId() ID
|
||||
GetRawUrl() string
|
||||
GetInfo() (*Info, error)
|
||||
GetStreamUrls() ([]*url.URL, error)
|
||||
GetPlatformCNName() string
|
||||
GetLastStartTime() time.Time
|
||||
SetLastStartTime(time.Time)
|
||||
}
|
||||
|
||||
type wrappedLive struct {
|
||||
Live
|
||||
cache gcache.Cache
|
||||
}
|
||||
|
||||
func newWrappedLive(live Live, cache gcache.Cache) Live {
|
||||
return &wrappedLive{
|
||||
Live: live,
|
||||
cache: cache,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *wrappedLive) GetInfo() (*Info, error) {
|
||||
i, err := w.Live.GetInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if w.cache != nil {
|
||||
w.cache.Set(w, i)
|
||||
}
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func New(url *url.URL, cache gcache.Cache) (live Live, err error) {
|
||||
builder, ok := getBuilder(url.Host)
|
||||
if !ok {
|
||||
return nil, errors.New("not support this url")
|
||||
}
|
||||
live, err = builder.Build(url)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
live = newWrappedLive(live, cache)
|
||||
for i := 0; i < 3; i++ {
|
||||
if _, err = live.GetInfo(); err == nil {
|
||||
break
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package longzhu
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/hr3lxphr6j/bililive-go/src/lib/http"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/lib/utils"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/live"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/live/internal"
|
||||
)
|
||||
|
||||
const (
|
||||
domain = "star.longzhu.com"
|
||||
cnName = "龙珠"
|
||||
|
||||
mobileUrl = "http://m.longzhu.com/"
|
||||
roomApiUrl = "http://liveapi.plu.cn/liveapp/roomstatus"
|
||||
liveApiUrl = "http://livestream.plu.cn/live/getlivePlayurl"
|
||||
)
|
||||
|
||||
func init() {
|
||||
live.Register(domain, new(builder))
|
||||
}
|
||||
|
||||
type builder struct{}
|
||||
|
||||
func (b *builder) Build(url *url.URL) (live.Live, error) {
|
||||
return &Live{
|
||||
BaseLive: internal.NewBaseLive(url),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type Live struct {
|
||||
internal.BaseLive
|
||||
realId string
|
||||
}
|
||||
|
||||
func (l *Live) parseRealId() error {
|
||||
paths := strings.Split(l.Url.Path, "/")
|
||||
if len(paths) < 2 {
|
||||
return live.ErrRoomUrlIncorrect
|
||||
}
|
||||
dom, err := http.Get(fmt.Sprintf("%s%s", mobileUrl, paths[1]), nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
realId := utils.Match1(`var\s*roomId\s*=\s*(\d+);`, string(dom))
|
||||
if realId == "" {
|
||||
return live.ErrRoomNotExist
|
||||
}
|
||||
l.realId = realId
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Live) GetInfo() (info *live.Info, err error) {
|
||||
if l.realId == "" {
|
||||
if err := l.parseRealId(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
body, err := http.Get(roomApiUrl, nil, map[string]string{"roomId": l.realId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info = &live.Info{
|
||||
Live: l,
|
||||
HostName: gjson.GetBytes(body, "userName").String(),
|
||||
RoomName: gjson.GetBytes(body, "title").String(),
|
||||
Status: len(gjson.GetBytes(body, "streamUri").String()) > 4,
|
||||
}
|
||||
return info, nil
|
||||
|
||||
}
|
||||
|
||||
func (l *Live) GetStreamUrls() (us []*url.URL, err error) {
|
||||
if l.realId == "" {
|
||||
if err := l.parseRealId(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
body, err := http.Get(liveApiUrl, nil, map[string]string{"roomId": l.realId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
urls := make([]string, 0, 0)
|
||||
gjson.GetBytes(body, "playLines.0.urls.#.securityUrl").ForEach(func(key, value gjson.Result) bool {
|
||||
urls = append(urls, value.String())
|
||||
return true
|
||||
})
|
||||
return utils.GenUrls(urls...)
|
||||
}
|
||||
|
||||
func (l *Live) GetPlatformCNName() string {
|
||||
return cnName
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/hr3lxphr6j/bililive-go/src/live (interfaces: Live)
|
||||
|
||||
// Package mock is a generated GoMock package.
|
||||
package mock
|
||||
|
||||
import (
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
live "github.com/hr3lxphr6j/bililive-go/src/live"
|
||||
url "net/url"
|
||||
reflect "reflect"
|
||||
time "time"
|
||||
)
|
||||
|
||||
// MockLive is a mock of Live interface
|
||||
type MockLive struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockLiveMockRecorder
|
||||
}
|
||||
|
||||
// MockLiveMockRecorder is the mock recorder for MockLive
|
||||
type MockLiveMockRecorder struct {
|
||||
mock *MockLive
|
||||
}
|
||||
|
||||
// NewMockLive creates a new mock instance
|
||||
func NewMockLive(ctrl *gomock.Controller) *MockLive {
|
||||
mock := &MockLive{ctrl: ctrl}
|
||||
mock.recorder = &MockLiveMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockLive) EXPECT() *MockLiveMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// GetInfo mocks base method
|
||||
func (m *MockLive) GetInfo() (*live.Info, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetInfo")
|
||||
ret0, _ := ret[0].(*live.Info)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetInfo indicates an expected call of GetInfo
|
||||
func (mr *MockLiveMockRecorder) GetInfo() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInfo", reflect.TypeOf((*MockLive)(nil).GetInfo))
|
||||
}
|
||||
|
||||
// GetLastStartTime mocks base method
|
||||
func (m *MockLive) GetLastStartTime() time.Time {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetLastStartTime")
|
||||
ret0, _ := ret[0].(time.Time)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetLastStartTime indicates an expected call of GetLastStartTime
|
||||
func (mr *MockLiveMockRecorder) GetLastStartTime() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLastStartTime", reflect.TypeOf((*MockLive)(nil).GetLastStartTime))
|
||||
}
|
||||
|
||||
// GetLiveId mocks base method
|
||||
func (m *MockLive) GetLiveId() live.ID {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetLiveId")
|
||||
ret0, _ := ret[0].(live.ID)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetLiveId indicates an expected call of GetLiveId
|
||||
func (mr *MockLiveMockRecorder) GetLiveId() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLiveId", reflect.TypeOf((*MockLive)(nil).GetLiveId))
|
||||
}
|
||||
|
||||
// GetPlatformCNName mocks base method
|
||||
func (m *MockLive) GetPlatformCNName() string {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetPlatformCNName")
|
||||
ret0, _ := ret[0].(string)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetPlatformCNName indicates an expected call of GetPlatformCNName
|
||||
func (mr *MockLiveMockRecorder) GetPlatformCNName() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPlatformCNName", reflect.TypeOf((*MockLive)(nil).GetPlatformCNName))
|
||||
}
|
||||
|
||||
// GetRawUrl mocks base method
|
||||
func (m *MockLive) GetRawUrl() string {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetRawUrl")
|
||||
ret0, _ := ret[0].(string)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetRawUrl indicates an expected call of GetRawUrl
|
||||
func (mr *MockLiveMockRecorder) GetRawUrl() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRawUrl", reflect.TypeOf((*MockLive)(nil).GetRawUrl))
|
||||
}
|
||||
|
||||
// GetStreamUrls mocks base method
|
||||
func (m *MockLive) GetStreamUrls() ([]*url.URL, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetStreamUrls")
|
||||
ret0, _ := ret[0].([]*url.URL)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetStreamUrls indicates an expected call of GetStreamUrls
|
||||
func (mr *MockLiveMockRecorder) GetStreamUrls() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetStreamUrls", reflect.TypeOf((*MockLive)(nil).GetStreamUrls))
|
||||
}
|
||||
|
||||
// SetLastStartTime mocks base method
|
||||
func (m *MockLive) SetLastStartTime(arg0 time.Time) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "SetLastStartTime", arg0)
|
||||
}
|
||||
|
||||
// SetLastStartTime indicates an expected call of SetLastStartTime
|
||||
func (mr *MockLiveMockRecorder) SetLastStartTime(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLastStartTime", reflect.TypeOf((*MockLive)(nil).SetLastStartTime), arg0)
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package openrec
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/hr3lxphr6j/bililive-go/src/lib/http"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/lib/utils"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/live"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/live/internal"
|
||||
)
|
||||
|
||||
const (
|
||||
domain = "www.openrec.tv"
|
||||
cnName = "openrec"
|
||||
)
|
||||
|
||||
type Live struct {
|
||||
internal.BaseLive
|
||||
}
|
||||
|
||||
func init() {
|
||||
live.Register(domain, new(builder))
|
||||
}
|
||||
|
||||
type builder struct{}
|
||||
|
||||
func (b *builder) Build(url *url.URL) (live.Live, error) {
|
||||
return &Live{
|
||||
BaseLive: internal.NewBaseLive(url),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (l *Live) GetInfo() (info *live.Info, err error) {
|
||||
dom, err := http.Get(l.Url.String(), nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var (
|
||||
roomName = utils.ParseString(
|
||||
utils.Match1(`"title":"([^:]*)",`, string(dom)),
|
||||
utils.StringFilterFunc(strings.TrimSpace),
|
||||
utils.UnescapeHTMLEntity,
|
||||
)
|
||||
hostName = utils.ParseString(
|
||||
utils.Match1(`"name":"([^:]*)",`, string(dom)),
|
||||
utils.ParseUnicode,
|
||||
utils.UnescapeHTMLEntity,
|
||||
)
|
||||
status = utils.Match1(`"onairStatus":(\d),`, string(dom))
|
||||
)
|
||||
if roomName == "" || hostName == "" || status == "" {
|
||||
return nil, live.ErrInternalError
|
||||
}
|
||||
info = &live.Info{
|
||||
Live: l,
|
||||
RoomName: roomName,
|
||||
HostName: hostName,
|
||||
Status: status == "1",
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (l *Live) GetStreamUrls() (us []*url.URL, err error) {
|
||||
dom, err := http.Get(l.Url.String(), nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return utils.GenUrls(utils.Match1(`{"url":"(\S*m3u8)",`, string(dom)))
|
||||
}
|
||||
|
||||
func (l *Live) GetPlatformCNName() string {
|
||||
return cnName
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package qq
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/hr3lxphr6j/bililive-go/src/lib/http"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/lib/utils"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/live"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/live/internal"
|
||||
)
|
||||
|
||||
const (
|
||||
domain = "egame.qq.com"
|
||||
cnName = "企鹅电竞"
|
||||
mobileUrl = "https://m.egame.qq.com/live"
|
||||
)
|
||||
|
||||
func init() {
|
||||
live.Register(domain, new(builder))
|
||||
}
|
||||
|
||||
type builder struct{}
|
||||
|
||||
func (b *builder) Build(url *url.URL) (live.Live, error) {
|
||||
return &Live{
|
||||
BaseLive: internal.NewBaseLive(url),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type Live struct {
|
||||
internal.BaseLive
|
||||
}
|
||||
|
||||
func (l *Live) GetInfo() (info *live.Info, err error) {
|
||||
paths := strings.Split(l.Url.Path, "/")
|
||||
if len(paths) < 2 {
|
||||
return nil, live.ErrRoomUrlIncorrect
|
||||
}
|
||||
anchorID := paths[1]
|
||||
dom, err := http.Get(l.Url.String(), nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
roomName := utils.UnescapeHTMLEntity(utils.Match1(`title:"([^"]*)"`, string(dom)))
|
||||
hostName := utils.UnescapeHTMLEntity(utils.Match1(`nickName:"([^"]+)"`, string(dom)))
|
||||
dom2, err := http.Get(mobileUrl, nil, map[string]string{
|
||||
"anchorid": anchorID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
isLive := utils.Match1(`"isLive":(\d+)`, string(dom2))
|
||||
if roomName == "" || hostName == "" || isLive == "" {
|
||||
return nil, live.ErrInternalError
|
||||
}
|
||||
info = &live.Info{
|
||||
Live: l,
|
||||
RoomName: string(roomName),
|
||||
HostName: string(hostName),
|
||||
Status: isLive == "1",
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (l *Live) GetStreamUrls() (us []*url.URL, err error) {
|
||||
dom, err := http.Get(l.Url.String(), nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := utils.Match1(`"urlArray":(\[[^\]]+\])`, string(dom))
|
||||
if result == "" {
|
||||
return nil, live.ErrInternalError
|
||||
}
|
||||
return utils.GenUrls(gjson.Get(result, "#[bitrate==0].playUrl").String())
|
||||
}
|
||||
|
||||
func (l *Live) GetPlatformCNName() string {
|
||||
return cnName
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package twitch
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/hr3lxphr6j/bililive-go/src/lib/http"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/live"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/live/internal"
|
||||
)
|
||||
|
||||
const (
|
||||
domain = "www.twitch.tv"
|
||||
cnName = "twitch"
|
||||
|
||||
clientId = "jzkbprff40iqj646a697cyrvl0zt2m6"
|
||||
channelApiUrl = "https://api.twitch.tv/kraken/channels/%s"
|
||||
liveBaseUrl = "https://usher.ttvnw.net/api/channel/hls/%s.m3u8"
|
||||
streamApiUrl = "https://api.twitch.tv/kraken/streams/%s"
|
||||
tokenApiUrl = "https://api.twitch.tv/api/channels/%s/access_token"
|
||||
)
|
||||
|
||||
func init() {
|
||||
live.Register(domain, new(builder))
|
||||
}
|
||||
|
||||
type builder struct{}
|
||||
|
||||
func (b *builder) Build(url *url.URL) (live.Live, error) {
|
||||
return &Live{
|
||||
BaseLive: internal.NewBaseLive(url),
|
||||
}, nil
|
||||
}
|
||||
|
||||
var headers = map[string]string{"client-id": clientId}
|
||||
|
||||
type Live struct {
|
||||
internal.BaseLive
|
||||
hostName, roomName string
|
||||
}
|
||||
|
||||
// 在hostName, roomName为空执行,在live有效时再从steam api解析
|
||||
func (l *Live) parseInfo() error {
|
||||
paths := strings.Split(l.Url.Path, "/")
|
||||
if len(paths) < 2 {
|
||||
return live.ErrRoomUrlIncorrect
|
||||
}
|
||||
chanId := paths[1]
|
||||
body, err := http.Get(fmt.Sprintf(channelApiUrl, chanId), headers, nil)
|
||||
if err != nil {
|
||||
return live.ErrRoomNotExist
|
||||
}
|
||||
l.hostName = gjson.GetBytes(body, "name").String()
|
||||
l.roomName = gjson.GetBytes(body, "status").String()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Live) GetInfo() (info *live.Info, err error) {
|
||||
if l.hostName == "" || l.roomName == "" {
|
||||
if err := l.parseInfo(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
body, err := http.Get(fmt.Sprintf(streamApiUrl, l.hostName), headers, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
status := gjson.GetBytes(body, "stream").String() != ""
|
||||
if status {
|
||||
l.roomName = gjson.GetBytes(body, "stream.channel.status").String()
|
||||
}
|
||||
info = &live.Info{
|
||||
Live: l,
|
||||
HostName: l.hostName,
|
||||
RoomName: l.roomName,
|
||||
Status: status,
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (l *Live) GetStreamUrls() (us []*url.URL, err error) {
|
||||
if l.hostName == "" || l.roomName == "" {
|
||||
if err := l.parseInfo(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
body, err := http.Get(fmt.Sprintf(tokenApiUrl, l.hostName), headers, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var (
|
||||
token = gjson.GetBytes(body, "token").String()
|
||||
sig = gjson.GetBytes(body, "sig").String()
|
||||
p = fmt.Sprintf("%d", rand.Intn(9000000)+1000000)
|
||||
)
|
||||
u, err := url.Parse(fmt.Sprintf(liveBaseUrl, l.hostName))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v := url.Values{}
|
||||
v.Add("allow_source", "true")
|
||||
v.Add("allow_audio_only", "true")
|
||||
v.Add("allow_spectre", "true")
|
||||
v.Add("p", p)
|
||||
v.Add("player", "twitchweb")
|
||||
v.Add("segment_preference", "4")
|
||||
v.Add("sig", sig)
|
||||
v.Add("token", token)
|
||||
u.RawQuery = v.Encode()
|
||||
return []*url.URL{u}, nil
|
||||
}
|
||||
|
||||
func (l *Live) GetPlatformCNName() string {
|
||||
return cnName
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package yizhibo
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/hr3lxphr6j/bililive-go/src/lib/http"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/lib/utils"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/live"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/live/internal"
|
||||
)
|
||||
|
||||
const (
|
||||
domain = "www.yizhibo.com"
|
||||
cnName = "一直播"
|
||||
|
||||
apiUrl = "http://www.yizhibo.com/live/h5api/get_basic_live_info"
|
||||
)
|
||||
|
||||
func init() {
|
||||
live.Register(domain, new(builder))
|
||||
}
|
||||
|
||||
type builder struct{}
|
||||
|
||||
func (b *builder) Build(url *url.URL) (live.Live, error) {
|
||||
return &Live{
|
||||
BaseLive: internal.NewBaseLive(url),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type Live struct {
|
||||
internal.BaseLive
|
||||
}
|
||||
|
||||
func (l *Live) requestRoomInfo() ([]byte, error) {
|
||||
scid := strings.Split(strings.Split(l.Url.Path, "/")[2], ".")[0]
|
||||
body, err := http.Get(apiUrl, nil, map[string]string{"scid": scid})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if gjson.GetBytes(body, "result").Int() != 1 {
|
||||
return nil, live.ErrRoomNotExist
|
||||
} else {
|
||||
return body, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Live) GetInfo() (info *live.Info, err error) {
|
||||
data, err := l.requestRoomInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info = &live.Info{
|
||||
Live: l,
|
||||
HostName: gjson.GetBytes(data, "data.nickname").String(),
|
||||
RoomName: gjson.GetBytes(data, "data.live_title").String(),
|
||||
Status: gjson.GetBytes(data, "data.status").Int() == 10,
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (l *Live) GetStreamUrls() (us []*url.URL, err error) {
|
||||
data, err := l.requestRoomInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return utils.GenUrls(gjson.GetBytes(data, "data.play_url").String())
|
||||
}
|
||||
|
||||
func (l *Live) GetPlatformCNName() string {
|
||||
return cnName
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package zhanqi
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/hr3lxphr6j/bililive-go/src/lib/http"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/lib/utils"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/live"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/live/internal"
|
||||
)
|
||||
|
||||
const (
|
||||
domain = "www.zhanqi.tv"
|
||||
cnName = "战旗"
|
||||
|
||||
apiUrl = "https://www.zhanqi.tv/api/static/v2.1/room/domain/%s.json"
|
||||
)
|
||||
|
||||
func init() {
|
||||
live.Register(domain, new(builder))
|
||||
}
|
||||
|
||||
type builder struct{}
|
||||
|
||||
func (b *builder) Build(url *url.URL) (live.Live, error) {
|
||||
return &Live{
|
||||
BaseLive: internal.NewBaseLive(url),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type Live struct {
|
||||
internal.BaseLive
|
||||
}
|
||||
|
||||
func (l *Live) requestRoomInfo() ([]byte, error) {
|
||||
body, err := http.Get(fmt.Sprintf(apiUrl, strings.Split(l.Url.Path, "/")[1]), nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if gjson.GetBytes(body, "code").Int() == 0 {
|
||||
return body, nil
|
||||
} else {
|
||||
return nil, live.ErrRoomNotExist
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Live) GetInfo() (info *live.Info, err error) {
|
||||
body, err := l.requestRoomInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info = &live.Info{
|
||||
Live: l,
|
||||
HostName: gjson.GetBytes(body, "data.nickname").String(),
|
||||
RoomName: gjson.GetBytes(body, "data.title").String(),
|
||||
Status: gjson.GetBytes(body, "data.status").String() == "4",
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (l *Live) GetStreamUrls() (us []*url.URL, err error) {
|
||||
body, err := l.requestRoomInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
videoLevels := gjson.GetBytes(body, "data.flashvars.VideoLevels").String()
|
||||
data, err := base64.StdEncoding.DecodeString(videoLevels)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return utils.GenUrls(gjson.GetBytes(data, "streamUrl").String())
|
||||
}
|
||||
|
||||
func (l *Live) GetPlatformCNName() string {
|
||||
return cnName
|
||||
}
|
||||
+1
-1
@@ -10,7 +10,7 @@ import (
|
||||
"github.com/hr3lxphr6j/bililive-go/src/interfaces"
|
||||
)
|
||||
|
||||
func NewLogger(ctx context.Context) *interfaces.Logger {
|
||||
func New(ctx context.Context) *interfaces.Logger {
|
||||
inst := instance.GetInstance(ctx)
|
||||
logLevel := logrus.InfoLevel
|
||||
if inst.Config.Debug {
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package recorders
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrRecorderExist = errors.New("recorder is exist")
|
||||
ErrRecorderNotExist = errors.New("recorder is not exist")
|
||||
)
|
||||
File renamed without changes.
@@ -0,0 +1,125 @@
|
||||
package recorders
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/hr3lxphr6j/bililive-go/src/instance"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/interfaces"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/lib/events"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/listeners"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/live"
|
||||
)
|
||||
|
||||
func NewManager(ctx context.Context) Manager {
|
||||
rm := &manager{
|
||||
savers: make(map[live.ID]Recorder),
|
||||
}
|
||||
instance.GetInstance(ctx).RecorderManager = rm
|
||||
return rm
|
||||
}
|
||||
|
||||
type Manager interface {
|
||||
interfaces.Module
|
||||
AddRecorder(ctx context.Context, live live.Live) error
|
||||
RemoveRecorder(ctx context.Context, liveId live.ID) error
|
||||
GetRecorder(ctx context.Context, liveId live.ID) (Recorder, error)
|
||||
HasRecorder(ctx context.Context, liveId live.ID) bool
|
||||
}
|
||||
|
||||
// for test
|
||||
var (
|
||||
newRecorder = NewRecorder
|
||||
)
|
||||
|
||||
type manager struct {
|
||||
lock sync.RWMutex
|
||||
savers map[live.ID]Recorder
|
||||
}
|
||||
|
||||
func (m *manager) registryListener(ctx context.Context, ed events.Dispatcher) {
|
||||
ed.AddEventListener(listeners.LiveStart, events.NewEventListener(func(event *events.Event) {
|
||||
live := event.Object.(live.Live)
|
||||
if err := m.AddRecorder(ctx, live); err != nil {
|
||||
instance.GetInstance(ctx).Logger.
|
||||
Errorf("failed to add recorder, err: %v", err)
|
||||
}
|
||||
}))
|
||||
|
||||
removeEvtListener := events.NewEventListener(func(event *events.Event) {
|
||||
live := event.Object.(live.Live)
|
||||
if !m.HasRecorder(ctx, live.GetLiveId()) {
|
||||
return
|
||||
}
|
||||
if err := m.RemoveRecorder(ctx, live.GetLiveId()); err != nil {
|
||||
instance.GetInstance(ctx).Logger.
|
||||
Errorf("failed to remove recorder, err: %v", err)
|
||||
}
|
||||
})
|
||||
ed.AddEventListener(listeners.LiveEnd, removeEvtListener)
|
||||
ed.AddEventListener(listeners.ListenStop, removeEvtListener)
|
||||
}
|
||||
|
||||
func (m *manager) Start(ctx context.Context) error {
|
||||
inst := instance.GetInstance(ctx)
|
||||
if inst.Config.RPC.Enable || len(inst.Lives) > 0 {
|
||||
inst.WaitGroup.Add(1)
|
||||
}
|
||||
m.registryListener(ctx, inst.EventDispatcher.(events.Dispatcher))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *manager) Close(ctx context.Context) {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
for id, recorder := range m.savers {
|
||||
recorder.Close()
|
||||
delete(m.savers, id)
|
||||
}
|
||||
inst := instance.GetInstance(ctx)
|
||||
inst.WaitGroup.Done()
|
||||
}
|
||||
|
||||
func (m *manager) AddRecorder(ctx context.Context, live live.Live) error {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
if _, ok := m.savers[live.GetLiveId()]; ok {
|
||||
return ErrRecorderExist
|
||||
}
|
||||
recorder, err := newRecorder(ctx, live)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.savers[live.GetLiveId()] = recorder
|
||||
return recorder.Start()
|
||||
|
||||
}
|
||||
|
||||
func (m *manager) RemoveRecorder(ctx context.Context, liveId live.ID) error {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
recorder, ok := m.savers[liveId]
|
||||
if !ok {
|
||||
return ErrRecorderNotExist
|
||||
}
|
||||
recorder.Close()
|
||||
delete(m.savers, liveId)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *manager) GetRecorder(ctx context.Context, liveId live.ID) (Recorder, error) {
|
||||
m.lock.RLock()
|
||||
defer m.lock.RUnlock()
|
||||
r, ok := m.savers[liveId]
|
||||
if !ok {
|
||||
return nil, ErrRecorderNotExist
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (m *manager) HasRecorder(ctx context.Context, liveId live.ID) bool {
|
||||
m.lock.RLock()
|
||||
defer m.lock.RUnlock()
|
||||
_, ok := m.savers[liveId]
|
||||
return ok
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package recorders
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/hr3lxphr6j/bililive-go/src/instance"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/live"
|
||||
livemock "github.com/hr3lxphr6j/bililive-go/src/live/mock"
|
||||
)
|
||||
|
||||
func TestManagerAddAndRemoveRecorder(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
ctx := context.WithValue(context.Background(), instance.InstanceKey, &instance.Instance{})
|
||||
m := NewManager(ctx)
|
||||
backup := newRecorder
|
||||
newRecorder = func(ctx context.Context, live live.Live) (Recorder, error) {
|
||||
r := NewMockRecorder(ctrl)
|
||||
r.EXPECT().Start().Return(nil)
|
||||
r.EXPECT().Close()
|
||||
return r, nil
|
||||
}
|
||||
defer func() { newRecorder = backup }()
|
||||
l := livemock.NewMockLive(ctrl)
|
||||
l.EXPECT().GetLiveId().Return(live.ID("test")).Times(3)
|
||||
assert.NoError(t, m.AddRecorder(context.Background(), l))
|
||||
assert.Equal(t, ErrRecorderExist, m.AddRecorder(context.Background(), l))
|
||||
ln, err := m.GetRecorder(context.Background(), "test")
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, ln)
|
||||
assert.True(t, m.HasRecorder(context.Background(), "test"))
|
||||
assert.NoError(t, m.RemoveRecorder(context.Background(), "test"))
|
||||
assert.Equal(t, ErrRecorderNotExist, m.RemoveRecorder(context.Background(), "test"))
|
||||
_, err = m.GetRecorder(context.Background(), "test")
|
||||
assert.Equal(t, ErrRecorderNotExist, err)
|
||||
assert.False(t, m.HasRecorder(context.Background(), "test"))
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/hr3lxphr6j/bililive-go/src/recorders (interfaces: Recorder,Manager)
|
||||
|
||||
// Package recorders is a generated GoMock package.
|
||||
package recorders
|
||||
|
||||
import (
|
||||
context "context"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
live "github.com/hr3lxphr6j/bililive-go/src/live"
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
// MockRecorder is a mock of Recorder interface
|
||||
type MockRecorder struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockRecorderMockRecorder
|
||||
}
|
||||
|
||||
// MockRecorderMockRecorder is the mock recorder for MockRecorder
|
||||
type MockRecorderMockRecorder struct {
|
||||
mock *MockRecorder
|
||||
}
|
||||
|
||||
// NewMockRecorder creates a new mock instance
|
||||
func NewMockRecorder(ctrl *gomock.Controller) *MockRecorder {
|
||||
mock := &MockRecorder{ctrl: ctrl}
|
||||
mock.recorder = &MockRecorderMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockRecorder) EXPECT() *MockRecorderMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Close mocks base method
|
||||
func (m *MockRecorder) Close() {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "Close")
|
||||
}
|
||||
|
||||
// Close indicates an expected call of Close
|
||||
func (mr *MockRecorderMockRecorder) Close() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockRecorder)(nil).Close))
|
||||
}
|
||||
|
||||
// Start mocks base method
|
||||
func (m *MockRecorder) Start() error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Start")
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Start indicates an expected call of Start
|
||||
func (mr *MockRecorderMockRecorder) Start() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockRecorder)(nil).Start))
|
||||
}
|
||||
|
||||
// MockManager is a mock of Manager interface
|
||||
type MockManager struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockManagerMockRecorder
|
||||
}
|
||||
|
||||
// MockManagerMockRecorder is the mock recorder for MockManager
|
||||
type MockManagerMockRecorder struct {
|
||||
mock *MockManager
|
||||
}
|
||||
|
||||
// NewMockManager creates a new mock instance
|
||||
func NewMockManager(ctrl *gomock.Controller) *MockManager {
|
||||
mock := &MockManager{ctrl: ctrl}
|
||||
mock.recorder = &MockManagerMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockManager) EXPECT() *MockManagerMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// AddRecorder mocks base method
|
||||
func (m *MockManager) AddRecorder(arg0 context.Context, arg1 live.Live) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "AddRecorder", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// AddRecorder indicates an expected call of AddRecorder
|
||||
func (mr *MockManagerMockRecorder) AddRecorder(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddRecorder", reflect.TypeOf((*MockManager)(nil).AddRecorder), arg0, arg1)
|
||||
}
|
||||
|
||||
// Close mocks base method
|
||||
func (m *MockManager) Close(arg0 context.Context) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "Close", arg0)
|
||||
}
|
||||
|
||||
// Close indicates an expected call of Close
|
||||
func (mr *MockManagerMockRecorder) Close(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockManager)(nil).Close), arg0)
|
||||
}
|
||||
|
||||
// GetRecorder mocks base method
|
||||
func (m *MockManager) GetRecorder(arg0 context.Context, arg1 live.ID) (Recorder, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetRecorder", arg0, arg1)
|
||||
ret0, _ := ret[0].(Recorder)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetRecorder indicates an expected call of GetRecorder
|
||||
func (mr *MockManagerMockRecorder) GetRecorder(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRecorder", reflect.TypeOf((*MockManager)(nil).GetRecorder), arg0, arg1)
|
||||
}
|
||||
|
||||
// HasRecorder mocks base method
|
||||
func (m *MockManager) HasRecorder(arg0 context.Context, arg1 live.ID) bool {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "HasRecorder", arg0, arg1)
|
||||
ret0, _ := ret[0].(bool)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// HasRecorder indicates an expected call of HasRecorder
|
||||
func (mr *MockManagerMockRecorder) HasRecorder(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasRecorder", reflect.TypeOf((*MockManager)(nil).HasRecorder), arg0, arg1)
|
||||
}
|
||||
|
||||
// RemoveRecorder mocks base method
|
||||
func (m *MockManager) RemoveRecorder(arg0 context.Context, arg1 live.ID) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "RemoveRecorder", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// RemoveRecorder indicates an expected call of RemoveRecorder
|
||||
func (mr *MockManagerMockRecorder) RemoveRecorder(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveRecorder", reflect.TypeOf((*MockManager)(nil).RemoveRecorder), arg0, arg1)
|
||||
}
|
||||
|
||||
// Start mocks base method
|
||||
func (m *MockManager) Start(arg0 context.Context) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Start", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Start indicates an expected call of Start
|
||||
func (mr *MockManagerMockRecorder) Start(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockManager)(nil).Start), arg0)
|
||||
}
|
||||
+100
-39
@@ -1,8 +1,10 @@
|
||||
//go:generate mockgen -package recorders -destination mock_test.go github.com/hr3lxphr6j/bililive-go/src/recorders Recorder,Manager
|
||||
package recorders
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -10,7 +12,9 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/hr3lxphr6j/bililive-go/src/api"
|
||||
"github.com/bluele/gcache"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/hr3lxphr6j/bililive-go/src/configs"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/instance"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/interfaces"
|
||||
@@ -19,6 +23,7 @@ import (
|
||||
"github.com/hr3lxphr6j/bililive-go/src/lib/parser/ffmpeg"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/lib/parser/native/flv"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/lib/utils"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/live"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -28,13 +33,40 @@ const (
|
||||
stopped
|
||||
)
|
||||
|
||||
type Recorder struct {
|
||||
Live api.Live
|
||||
// for test
|
||||
var (
|
||||
newParser = func(u *url.URL, useNativeFlvParser bool) (parser.Parser, error) {
|
||||
parserName := ffmpeg.Name
|
||||
if strings.Contains(u.Path, ".flv") && useNativeFlvParser {
|
||||
parserName = flv.Name
|
||||
}
|
||||
return parser.New(parserName)
|
||||
}
|
||||
|
||||
mkdir = func(path string) error {
|
||||
return os.MkdirAll(path, os.ModePerm)
|
||||
}
|
||||
|
||||
removeEmptyFile = func(file string) {
|
||||
if stat, err := os.Stat(file); err == nil && stat.Size() == 0 {
|
||||
os.Remove(file)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
type Recorder interface {
|
||||
Start() error
|
||||
Close()
|
||||
}
|
||||
|
||||
type recorder struct {
|
||||
Live live.Live
|
||||
OutPutPath string
|
||||
|
||||
config *configs.Config
|
||||
ed events.IEventDispatcher
|
||||
ed events.Dispatcher
|
||||
logger *interfaces.Logger
|
||||
cache gcache.Cache
|
||||
|
||||
parser parser.Parser
|
||||
parserLock *sync.RWMutex
|
||||
@@ -43,13 +75,14 @@ type Recorder struct {
|
||||
state uint32
|
||||
}
|
||||
|
||||
func NewRecorder(ctx context.Context, live api.Live) (*Recorder, error) {
|
||||
func NewRecorder(ctx context.Context, live live.Live) (Recorder, error) {
|
||||
inst := instance.GetInstance(ctx)
|
||||
return &Recorder{
|
||||
return &recorder{
|
||||
Live: live,
|
||||
OutPutPath: instance.GetInstance(ctx).Config.OutPutPath,
|
||||
config: inst.Config,
|
||||
ed: inst.EventDispatcher.(events.IEventDispatcher),
|
||||
cache: inst.Cache,
|
||||
ed: inst.EventDispatcher.(events.Dispatcher),
|
||||
logger: inst.Logger,
|
||||
state: begin,
|
||||
stop: make(chan struct{}),
|
||||
@@ -57,47 +90,59 @@ func NewRecorder(ctx context.Context, live api.Live) (*Recorder, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *Recorder) run() {
|
||||
func (r *recorder) tryRecode() {
|
||||
urls, err := r.Live.GetStreamUrls()
|
||||
if err != nil || len(urls) == 0 {
|
||||
r.getLogger().WithError(err).Warn("failed to get stream url, will retry after 5s...")
|
||||
time.Sleep(5 * time.Second)
|
||||
return
|
||||
}
|
||||
|
||||
obj, _ := r.cache.Get(r.Live)
|
||||
info := obj.(*live.Info)
|
||||
var (
|
||||
strFilter = utils.NewStringFilterChain(utils.ReplaceIllegalChar, utils.UnescapeHTMLEntity)
|
||||
platformName = strFilter.Do(r.Live.GetPlatformCNName())
|
||||
hostName = strFilter.Do(info.HostName)
|
||||
roomName = strFilter.Do(info.RoomName)
|
||||
ts = time.Now().Format("2006-01-02 15-04-05")
|
||||
fileName = fmt.Sprintf("[%s][%s][%s].flv", ts, hostName, roomName)
|
||||
outputPath = filepath.Join(r.OutPutPath, platformName, hostName)
|
||||
file = filepath.Join(outputPath, fileName)
|
||||
url = urls[0]
|
||||
)
|
||||
if err := mkdir(outputPath); err != nil {
|
||||
r.getLogger().WithError(err).Errorf("failed to create output path[%s]", outputPath)
|
||||
return
|
||||
}
|
||||
p, err := newParser(url, r.config.Feature.UseNativeFlvParser)
|
||||
if err != nil {
|
||||
r.getLogger().WithError(err).Error("failed to init parse")
|
||||
return
|
||||
}
|
||||
r.setAndCloseParser(p)
|
||||
r.getLogger().Debugln(r.parser.ParseLiveStream(url, r.Live, file))
|
||||
removeEmptyFile(file)
|
||||
}
|
||||
|
||||
func (r *recorder) run() {
|
||||
for {
|
||||
select {
|
||||
case <-r.stop:
|
||||
return
|
||||
default:
|
||||
urls, err := r.Live.GetStreamUrls()
|
||||
if err != nil {
|
||||
time.Sleep(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
var (
|
||||
platformName = utils.ReplaceIllegalChar(r.Live.GetPlatformCNName())
|
||||
hostName = utils.ReplaceIllegalChar(r.Live.GetCachedInfo().HostName)
|
||||
roomName = utils.ReplaceIllegalChar(r.Live.GetCachedInfo().RoomName)
|
||||
fileName = fmt.Sprintf("[%s][%s][%s].flv", time.Now().Format("2006-01-02 15-04-05"), hostName, roomName)
|
||||
outputPath = filepath.Join(r.OutPutPath, platformName, hostName)
|
||||
file = filepath.Join(outputPath, fileName)
|
||||
url = urls[0]
|
||||
)
|
||||
os.MkdirAll(outputPath, os.ModePerm)
|
||||
if strings.Contains(url.Path, ".flv") && r.config.Feature.UseNativeFlvParser {
|
||||
r.setAndCloseParser(flv.NewParser())
|
||||
} else {
|
||||
r.setAndCloseParser(ffmpeg.New())
|
||||
}
|
||||
r.logger.Debugln(r.parser.ParseLiveStream(url, file))
|
||||
if stat, err := os.Stat(file); err == nil && stat.Size() == 0 {
|
||||
os.Remove(file)
|
||||
}
|
||||
r.tryRecode()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Recorder) getParser() parser.Parser {
|
||||
func (r *recorder) getParser() parser.Parser {
|
||||
r.parserLock.RLock()
|
||||
defer r.parserLock.RUnlock()
|
||||
return r.parser
|
||||
}
|
||||
|
||||
func (r *Recorder) setAndCloseParser(p parser.Parser) {
|
||||
func (r *recorder) setAndCloseParser(p parser.Parser) {
|
||||
r.parserLock.Lock()
|
||||
defer r.parserLock.Unlock()
|
||||
if r.parser != nil {
|
||||
@@ -106,18 +151,18 @@ func (r *Recorder) setAndCloseParser(p parser.Parser) {
|
||||
r.parser = p
|
||||
}
|
||||
|
||||
func (r *Recorder) Start() error {
|
||||
func (r *recorder) Start() error {
|
||||
if !atomic.CompareAndSwapUint32(&r.state, begin, pending) {
|
||||
return fmt.Errorf("recorder in error state")
|
||||
return nil
|
||||
}
|
||||
go r.run()
|
||||
r.logger.WithFields(r.Live.GetInfoMap()).Info("Recorde Start")
|
||||
r.getLogger().Info("Record Start")
|
||||
r.ed.DispatchEvent(events.NewEvent(RecorderStart, r.Live))
|
||||
atomic.CompareAndSwapUint32(&r.state, pending, running)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Recorder) Close() {
|
||||
func (r *recorder) Close() {
|
||||
if !atomic.CompareAndSwapUint32(&r.state, running, stopped) {
|
||||
return
|
||||
}
|
||||
@@ -125,6 +170,22 @@ func (r *Recorder) Close() {
|
||||
if p := r.getParser(); p != nil {
|
||||
p.Stop()
|
||||
}
|
||||
r.logger.WithFields(r.Live.GetInfoMap()).Info("Recorde End")
|
||||
r.getLogger().Info("Record End")
|
||||
r.ed.DispatchEvent(events.NewEvent(RecorderStop, r.Live))
|
||||
}
|
||||
|
||||
func (r *recorder) getLogger() *logrus.Entry {
|
||||
return r.logger.WithFields(r.getFields())
|
||||
}
|
||||
|
||||
func (r *recorder) getFields() map[string]interface{} {
|
||||
obj, err := r.cache.Get(r.Live)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
info := obj.(*live.Info)
|
||||
return map[string]interface{}{
|
||||
"host": info.HostName,
|
||||
"room": info.RoomName,
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package recorders
|
||||
|
||||
import "errors"
|
||||
|
||||
var recorderExistError = errors.New("this recorder is exist")
|
||||
var recorderNotExistError = errors.New("this recorder is not exist")
|
||||
@@ -1,136 +0,0 @@
|
||||
package recorders
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/hr3lxphr6j/bililive-go/src/api"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/instance"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/lib/events"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/listeners"
|
||||
)
|
||||
|
||||
func NewIRecorderManager(ctx context.Context) IRecorderManager {
|
||||
rm := &RecorderManager{
|
||||
savers: make(map[api.LiveId]*Recorder),
|
||||
lock: new(sync.RWMutex),
|
||||
}
|
||||
instance.GetInstance(ctx).RecorderManager = rm
|
||||
return rm
|
||||
}
|
||||
|
||||
type IRecorderManager interface {
|
||||
AddRecorder(ctx context.Context, live api.Live) error
|
||||
RemoveRecorder(ctx context.Context, liveId api.LiveId) error
|
||||
GetRecorder(ctx context.Context, liveId api.LiveId) (*Recorder, error)
|
||||
HasRecorder(ctx context.Context, liveId api.LiveId) bool
|
||||
}
|
||||
|
||||
type RecorderManager struct {
|
||||
savers map[api.LiveId]*Recorder
|
||||
lock *sync.RWMutex
|
||||
}
|
||||
|
||||
func (r *RecorderManager) Start(ctx context.Context) error {
|
||||
inst := instance.GetInstance(ctx)
|
||||
if inst.Config.RPC.Enable || len(inst.Lives) > 0 {
|
||||
inst.WaitGroup.Add(1)
|
||||
}
|
||||
inst.Logger.Info("RecorderManager Start")
|
||||
ed := inst.EventDispatcher.(events.IEventDispatcher)
|
||||
|
||||
// 开播事件
|
||||
ed.AddEventListener(listeners.LiveStart, events.NewEventListener(func(event *events.Event) {
|
||||
live := event.Object.(api.Live)
|
||||
if err := r.AddRecorder(ctx, live); err != nil {
|
||||
instance.GetInstance(ctx).Logger.
|
||||
WithFields(live.GetInfoMap()).
|
||||
Errorf("failed to add recorder, err: %v", err)
|
||||
}
|
||||
}))
|
||||
|
||||
// 下播事件
|
||||
ed.AddEventListener(listeners.LiveEnd, events.NewEventListener(func(event *events.Event) {
|
||||
live := event.Object.(api.Live)
|
||||
if !r.HasRecorder(ctx, live.GetLiveId()) {
|
||||
return
|
||||
}
|
||||
if err := r.RemoveRecorder(ctx, live.GetLiveId()); err != nil {
|
||||
instance.GetInstance(ctx).Logger.
|
||||
WithFields(live.GetInfoMap()).
|
||||
Errorf("failed to remove recorder, err: %v", err)
|
||||
}
|
||||
}))
|
||||
|
||||
// 监听关闭事件
|
||||
ed.AddEventListener(listeners.ListenStop, events.NewEventListener(func(event *events.Event) {
|
||||
live := event.Object.(api.Live)
|
||||
if !r.HasRecorder(ctx, live.GetLiveId()) {
|
||||
return
|
||||
}
|
||||
if err := r.RemoveRecorder(ctx, live.GetLiveId()); err != nil {
|
||||
instance.GetInstance(ctx).Logger.
|
||||
WithFields(live.GetInfoMap()).
|
||||
Errorf("failed to remove recorder, err: %v", err)
|
||||
}
|
||||
}))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *RecorderManager) Close(ctx context.Context) {
|
||||
r.lock.Lock()
|
||||
defer r.lock.Unlock()
|
||||
for id, recorder := range r.savers {
|
||||
recorder.Close()
|
||||
delete(r.savers, id)
|
||||
}
|
||||
inst := instance.GetInstance(ctx)
|
||||
inst.WaitGroup.Done()
|
||||
inst.Logger.Info("RecorderManager Closed")
|
||||
}
|
||||
|
||||
func (r *RecorderManager) AddRecorder(ctx context.Context, live api.Live) error {
|
||||
r.lock.Lock()
|
||||
defer r.lock.Unlock()
|
||||
if _, ok := r.savers[live.GetLiveId()]; ok {
|
||||
return recorderExistError
|
||||
}
|
||||
recorder, err := NewRecorder(ctx, live)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.savers[live.GetLiveId()] = recorder
|
||||
recorder.Start()
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (r *RecorderManager) RemoveRecorder(ctx context.Context, liveId api.LiveId) error {
|
||||
r.lock.Lock()
|
||||
defer r.lock.Unlock()
|
||||
if recorder, ok := r.savers[liveId]; !ok {
|
||||
return recorderNotExistError
|
||||
} else {
|
||||
recorder.Close()
|
||||
delete(r.savers, liveId)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RecorderManager) GetRecorder(ctx context.Context, liveId api.LiveId) (*Recorder, error) {
|
||||
r.lock.RLock()
|
||||
defer r.lock.RUnlock()
|
||||
if r, ok := r.savers[liveId]; !ok {
|
||||
return nil, recorderNotExistError
|
||||
} else {
|
||||
return r, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RecorderManager) HasRecorder(ctx context.Context, liveId api.LiveId) bool {
|
||||
r.lock.RLock()
|
||||
defer r.lock.RUnlock()
|
||||
_, ok := r.savers[liveId]
|
||||
return ok
|
||||
}
|
||||
+16
-15
@@ -11,10 +11,10 @@ import (
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/hr3lxphr6j/bililive-go/src/api"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/consts"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/instance"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/listeners"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/live"
|
||||
"github.com/hr3lxphr6j/bililive-go/src/recorders"
|
||||
)
|
||||
|
||||
@@ -24,17 +24,18 @@ type CommonResp struct {
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
func parseInfo(ctx context.Context, live api.Live) *api.Info {
|
||||
func parseInfo(ctx context.Context, l live.Live) *live.Info {
|
||||
inst := instance.GetInstance(ctx)
|
||||
info := live.GetCachedInfo()
|
||||
info.Listening = inst.ListenerManager.(listeners.IListenerManager).HasListener(ctx, api.LiveId(live.GetLiveId()))
|
||||
info.Recoding = inst.RecorderManager.(recorders.IRecorderManager).HasRecorder(ctx, api.LiveId(live.GetLiveId()))
|
||||
obj, _ := inst.Cache.Get(l)
|
||||
info := obj.(*live.Info)
|
||||
info.Listening = inst.ListenerManager.(listeners.Manager).HasListener(ctx, l.GetLiveId())
|
||||
info.Recoding = inst.RecorderManager.(recorders.Manager).HasRecorder(ctx, l.GetLiveId())
|
||||
return info
|
||||
}
|
||||
|
||||
func getAllLives(writer http.ResponseWriter, r *http.Request) {
|
||||
inst := instance.GetInstance(r.Context())
|
||||
info := make([]*api.Info, 0)
|
||||
info := make([]*live.Info, 0)
|
||||
for _, v := range inst.Lives {
|
||||
info = append(info, parseInfo(r.Context(), v))
|
||||
}
|
||||
@@ -50,7 +51,7 @@ func getLive(writer http.ResponseWriter, r *http.Request) {
|
||||
inst := instance.GetInstance(r.Context())
|
||||
vars := mux.Vars(r)
|
||||
resp := CommonResp{}
|
||||
if live, ok := inst.Lives[api.LiveId(vars["id"])]; ok {
|
||||
if live, ok := inst.Lives[live.ID(vars["id"])]; ok {
|
||||
resp.Data = parseInfo(r.Context(), live)
|
||||
} else {
|
||||
resp.ErrNo = 404
|
||||
@@ -66,13 +67,13 @@ func parseLiveAction(writer http.ResponseWriter, r *http.Request) {
|
||||
inst := instance.GetInstance(r.Context())
|
||||
vars := mux.Vars(r)
|
||||
resp := CommonResp{}
|
||||
if live, ok := inst.Lives[api.LiveId(vars["id"])]; ok {
|
||||
if live, ok := inst.Lives[live.ID(vars["id"])]; ok {
|
||||
switch vars["action"] {
|
||||
case "start":
|
||||
inst.ListenerManager.(listeners.IListenerManager).AddListener(r.Context(), live)
|
||||
inst.ListenerManager.(listeners.Manager).AddListener(r.Context(), live)
|
||||
resp.Data = parseInfo(r.Context(), live)
|
||||
case "stop":
|
||||
inst.ListenerManager.(listeners.IListenerManager).RemoveListener(r.Context(), live.GetLiveId())
|
||||
inst.ListenerManager.(listeners.Manager).RemoveListener(r.Context(), live.GetLiveId())
|
||||
resp.Data = parseInfo(r.Context(), live)
|
||||
default:
|
||||
resp.ErrNo = 400
|
||||
@@ -106,16 +107,16 @@ func parseLiveAction(writer http.ResponseWriter, r *http.Request) {
|
||||
*/
|
||||
func addLives(writer http.ResponseWriter, r *http.Request) {
|
||||
b, _ := ioutil.ReadAll(r.Body)
|
||||
info := make([]*api.Info, 0)
|
||||
info := make([]*live.Info, 0)
|
||||
gjson.GetBytes(b, "lives").ForEach(func(key, value gjson.Result) bool {
|
||||
isListen := value.Get("listen").Bool()
|
||||
u, _ := url.Parse(value.Get("url").String())
|
||||
if live, err := api.NewLive(u); err == nil {
|
||||
if live, err := live.New(u, instance.GetInstance(r.Context()).Cache); err == nil {
|
||||
inst := instance.GetInstance(r.Context())
|
||||
if _, ok := inst.Lives[live.GetLiveId()]; !ok {
|
||||
inst.Lives[live.GetLiveId()] = live
|
||||
if isListen {
|
||||
inst.ListenerManager.(listeners.IListenerManager).AddListener(r.Context(), live)
|
||||
inst.ListenerManager.(listeners.Manager).AddListener(r.Context(), live)
|
||||
}
|
||||
info = append(info, parseInfo(r.Context(), live))
|
||||
}
|
||||
@@ -163,8 +164,8 @@ func removeLive(writer http.ResponseWriter, r *http.Request) {
|
||||
inst := instance.GetInstance(r.Context())
|
||||
vars := mux.Vars(r)
|
||||
res := CommonResp{}
|
||||
if live, ok := inst.Lives[api.LiveId(vars["id"])]; ok {
|
||||
inst.ListenerManager.(listeners.IListenerManager).RemoveListener(r.Context(), live.GetLiveId())
|
||||
if live, ok := inst.Lives[live.ID(vars["id"])]; ok {
|
||||
inst.ListenerManager.(listeners.Manager).RemoveListener(r.Context(), live.GetLiveId())
|
||||
delete(inst.Lives, live.GetLiveId())
|
||||
res.Data = "OK"
|
||||
} else {
|
||||
|
||||
+9
-11
@@ -105,7 +105,7 @@ func NewServer(ctx context.Context) *Server {
|
||||
inst := instance.GetInstance(ctx)
|
||||
config := inst.Config
|
||||
httpServer := &http.Server{
|
||||
Addr: config.RPC.Port,
|
||||
Addr: config.RPC.Bind,
|
||||
Handler: initMux(ctx),
|
||||
}
|
||||
server := &Server{server: httpServer}
|
||||
@@ -118,18 +118,16 @@ func (s *Server) Start(ctx context.Context) error {
|
||||
inst.WaitGroup.Add(1)
|
||||
config := inst.Config
|
||||
go func() {
|
||||
var err error
|
||||
if config.RPC.TLS.Enable {
|
||||
if err := s.server.ListenAndServeTLS(config.RPC.TLS.CertFile, config.RPC.TLS.KeyFile); err != nil {
|
||||
if err != http.ErrServerClosed {
|
||||
inst.Logger.Error(err)
|
||||
}
|
||||
}
|
||||
err = s.server.ListenAndServeTLS(config.RPC.TLS.CertFile, config.RPC.TLS.KeyFile)
|
||||
} else {
|
||||
if err := s.server.ListenAndServe(); err != nil {
|
||||
if err != http.ErrServerClosed {
|
||||
inst.Logger.Error(err)
|
||||
}
|
||||
}
|
||||
err = s.server.ListenAndServe()
|
||||
}
|
||||
switch err {
|
||||
case nil, http.ErrServerClosed:
|
||||
default:
|
||||
inst.Logger.Error(err)
|
||||
}
|
||||
}()
|
||||
inst.Logger.Infof("Server start at %s", s.server.Addr)
|
||||
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
Copyright (C) 2014 Alec Thomas
|
||||
|
||||
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.
|
||||
-674
@@ -1,674 +0,0 @@
|
||||
# Kingpin - A Go (golang) command line and flag parser
|
||||
[](http://godoc.org/github.com/alecthomas/kingpin) [](https://travis-ci.org/alecthomas/kingpin) [](https://gitter.im/alecthomas/Lobby)
|
||||
|
||||
|
||||
|
||||
<!-- MarkdownTOC -->
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Features](#features)
|
||||
- [User-visible changes between v1 and v2](#user-visible-changes-between-v1-and-v2)
|
||||
- [Flags can be used at any point after their definition.](#flags-can-be-used-at-any-point-after-their-definition)
|
||||
- [Short flags can be combined with their parameters](#short-flags-can-be-combined-with-their-parameters)
|
||||
- [API changes between v1 and v2](#api-changes-between-v1-and-v2)
|
||||
- [Versions](#versions)
|
||||
- [V2 is the current stable version](#v2-is-the-current-stable-version)
|
||||
- [V1 is the OLD stable version](#v1-is-the-old-stable-version)
|
||||
- [Change History](#change-history)
|
||||
- [Examples](#examples)
|
||||
- [Simple Example](#simple-example)
|
||||
- [Complex Example](#complex-example)
|
||||
- [Reference Documentation](#reference-documentation)
|
||||
- [Displaying errors and usage information](#displaying-errors-and-usage-information)
|
||||
- [Sub-commands](#sub-commands)
|
||||
- [Custom Parsers](#custom-parsers)
|
||||
- [Repeatable flags](#repeatable-flags)
|
||||
- [Boolean Values](#boolean-values)
|
||||
- [Default Values](#default-values)
|
||||
- [Place-holders in Help](#place-holders-in-help)
|
||||
- [Consuming all remaining arguments](#consuming-all-remaining-arguments)
|
||||
- [Bash/ZSH Shell Completion](#bashzsh-shell-completion)
|
||||
- [Supporting -h for help](#supporting--h-for-help)
|
||||
- [Custom help](#custom-help)
|
||||
|
||||
<!-- /MarkdownTOC -->
|
||||
|
||||
## Overview
|
||||
|
||||
Kingpin is a [fluent-style](http://en.wikipedia.org/wiki/Fluent_interface),
|
||||
type-safe command-line parser. It supports flags, nested commands, and
|
||||
positional arguments.
|
||||
|
||||
Install it with:
|
||||
|
||||
$ go get gopkg.in/alecthomas/kingpin.v2
|
||||
|
||||
It looks like this:
|
||||
|
||||
```go
|
||||
var (
|
||||
verbose = kingpin.Flag("verbose", "Verbose mode.").Short('v').Bool()
|
||||
name = kingpin.Arg("name", "Name of user.").Required().String()
|
||||
)
|
||||
|
||||
func main() {
|
||||
kingpin.Parse()
|
||||
fmt.Printf("%v, %s\n", *verbose, *name)
|
||||
}
|
||||
```
|
||||
|
||||
More [examples](https://github.com/alecthomas/kingpin/tree/master/_examples) are available.
|
||||
|
||||
Second to parsing, providing the user with useful help is probably the most
|
||||
important thing a command-line parser does. Kingpin tries to provide detailed
|
||||
contextual help if `--help` is encountered at any point in the command line
|
||||
(excluding after `--`).
|
||||
|
||||
## Features
|
||||
|
||||
- Help output that isn't as ugly as sin.
|
||||
- Fully [customisable help](#custom-help), via Go templates.
|
||||
- Parsed, type-safe flags (`kingpin.Flag("f", "help").Int()`)
|
||||
- Parsed, type-safe positional arguments (`kingpin.Arg("a", "help").Int()`).
|
||||
- Parsed, type-safe, arbitrarily deep commands (`kingpin.Command("c", "help")`).
|
||||
- Support for required flags and required positional arguments (`kingpin.Flag("f", "").Required().Int()`).
|
||||
- Support for arbitrarily nested default commands (`command.Default()`).
|
||||
- Callbacks per command, flag and argument (`kingpin.Command("c", "").Action(myAction)`).
|
||||
- POSIX-style short flag combining (`-a -b` -> `-ab`).
|
||||
- Short-flag+parameter combining (`-a parm` -> `-aparm`).
|
||||
- Read command-line from files (`@<file>`).
|
||||
- Automatically generate man pages (`--help-man`).
|
||||
|
||||
## User-visible changes between v1 and v2
|
||||
|
||||
### Flags can be used at any point after their definition.
|
||||
|
||||
Flags can be specified at any point after their definition, not just
|
||||
*immediately after their associated command*. From the chat example below, the
|
||||
following used to be required:
|
||||
|
||||
```
|
||||
$ chat --server=chat.server.com:8080 post --image=~/Downloads/owls.jpg pics
|
||||
```
|
||||
|
||||
But the following will now work:
|
||||
|
||||
```
|
||||
$ chat post --server=chat.server.com:8080 --image=~/Downloads/owls.jpg pics
|
||||
```
|
||||
|
||||
### Short flags can be combined with their parameters
|
||||
|
||||
Previously, if a short flag was used, any argument to that flag would have to
|
||||
be separated by a space. That is no longer the case.
|
||||
|
||||
## API changes between v1 and v2
|
||||
|
||||
- `ParseWithFileExpansion()` is gone. The new parser directly supports expanding `@<file>`.
|
||||
- Added `FatalUsage()` and `FatalUsageContext()` for displaying an error + usage and terminating.
|
||||
- `Dispatch()` renamed to `Action()`.
|
||||
- Added `ParseContext()` for parsing a command line into its intermediate context form without executing.
|
||||
- Added `Terminate()` function to override the termination function.
|
||||
- Added `UsageForContextWithTemplate()` for printing usage via a custom template.
|
||||
- Added `UsageTemplate()` for overriding the default template to use. Two templates are included:
|
||||
1. `DefaultUsageTemplate` - default template.
|
||||
2. `CompactUsageTemplate` - compact command template for larger applications.
|
||||
|
||||
## Versions
|
||||
|
||||
Kingpin uses [gopkg.in](https://gopkg.in/alecthomas/kingpin) for versioning.
|
||||
|
||||
The current stable version is [gopkg.in/alecthomas/kingpin.v2](https://gopkg.in/alecthomas/kingpin.v2). The previous version, [gopkg.in/alecthomas/kingpin.v1](https://gopkg.in/alecthomas/kingpin.v1), is deprecated and in maintenance mode.
|
||||
|
||||
### [V2](https://gopkg.in/alecthomas/kingpin.v2) is the current stable version
|
||||
|
||||
Installation:
|
||||
|
||||
```sh
|
||||
$ go get gopkg.in/alecthomas/kingpin.v2
|
||||
```
|
||||
|
||||
### [V1](https://gopkg.in/alecthomas/kingpin.v1) is the OLD stable version
|
||||
|
||||
Installation:
|
||||
|
||||
```sh
|
||||
$ go get gopkg.in/alecthomas/kingpin.v1
|
||||
```
|
||||
|
||||
## Change History
|
||||
|
||||
- *2015-09-19* -- Stable v2.1.0 release.
|
||||
- Added `command.Default()` to specify a default command to use if no other
|
||||
command matches. This allows for convenient user shortcuts.
|
||||
- Exposed `HelpFlag` and `VersionFlag` for further customisation.
|
||||
- `Action()` and `PreAction()` added and both now support an arbitrary
|
||||
number of callbacks.
|
||||
- `kingpin.SeparateOptionalFlagsUsageTemplate`.
|
||||
- `--help-long` and `--help-man` (hidden by default) flags.
|
||||
- Flags are "interspersed" by default, but can be disabled with `app.Interspersed(false)`.
|
||||
- Added flags for all simple builtin types (int8, uint16, etc.) and slice variants.
|
||||
- Use `app.Writer(os.Writer)` to specify the default writer for all output functions.
|
||||
- Dropped `os.Writer` prefix from all printf-like functions.
|
||||
|
||||
- *2015-05-22* -- Stable v2.0.0 release.
|
||||
- Initial stable release of v2.0.0.
|
||||
- Fully supports interspersed flags, commands and arguments.
|
||||
- Flags can be present at any point after their logical definition.
|
||||
- Application.Parse() terminates if commands are present and a command is not parsed.
|
||||
- Dispatch() -> Action().
|
||||
- Actions are dispatched after all values are populated.
|
||||
- Override termination function (defaults to os.Exit).
|
||||
- Override output stream (defaults to os.Stderr).
|
||||
- Templatised usage help, with default and compact templates.
|
||||
- Make error/usage functions more consistent.
|
||||
- Support argument expansion from files by default (with @<file>).
|
||||
- Fully public data model is available via .Model().
|
||||
- Parser has been completely refactored.
|
||||
- Parsing and execution has been split into distinct stages.
|
||||
- Use `go generate` to generate repeated flags.
|
||||
- Support combined short-flag+argument: -fARG.
|
||||
|
||||
- *2015-01-23* -- Stable v1.3.4 release.
|
||||
- Support "--" for separating flags from positional arguments.
|
||||
- Support loading flags from files (ParseWithFileExpansion()). Use @FILE as an argument.
|
||||
- Add post-app and post-cmd validation hooks. This allows arbitrary validation to be added.
|
||||
- A bunch of improvements to help usage and formatting.
|
||||
- Support arbitrarily nested sub-commands.
|
||||
|
||||
- *2014-07-08* -- Stable v1.2.0 release.
|
||||
- Pass any value through to `Strings()` when final argument.
|
||||
Allows for values that look like flags to be processed.
|
||||
- Allow `--help` to be used with commands.
|
||||
- Support `Hidden()` flags.
|
||||
- Parser for [units.Base2Bytes](https://github.com/alecthomas/units)
|
||||
type. Allows for flags like `--ram=512MB` or `--ram=1GB`.
|
||||
- Add an `Enum()` value, allowing only one of a set of values
|
||||
to be selected. eg. `Flag(...).Enum("debug", "info", "warning")`.
|
||||
|
||||
- *2014-06-27* -- Stable v1.1.0 release.
|
||||
- Bug fixes.
|
||||
- Always return an error (rather than panicing) when misconfigured.
|
||||
- `OpenFile(flag, perm)` value type added, for finer control over opening files.
|
||||
- Significantly improved usage formatting.
|
||||
|
||||
- *2014-06-19* -- Stable v1.0.0 release.
|
||||
- Support [cumulative positional](#consuming-all-remaining-arguments) arguments.
|
||||
- Return error rather than panic when there are fatal errors not caught by
|
||||
the type system. eg. when a default value is invalid.
|
||||
- Use gokpg.in.
|
||||
|
||||
- *2014-06-10* -- Place-holder streamlining.
|
||||
- Renamed `MetaVar` to `PlaceHolder`.
|
||||
- Removed `MetaVarFromDefault`. Kingpin now uses [heuristics](#place-holders-in-help)
|
||||
to determine what to display.
|
||||
|
||||
## Examples
|
||||
|
||||
### Simple Example
|
||||
|
||||
Kingpin can be used for simple flag+arg applications like so:
|
||||
|
||||
```
|
||||
$ ping --help
|
||||
usage: ping [<flags>] <ip> [<count>]
|
||||
|
||||
Flags:
|
||||
--debug Enable debug mode.
|
||||
--help Show help.
|
||||
-t, --timeout=5s Timeout waiting for ping.
|
||||
|
||||
Args:
|
||||
<ip> IP address to ping.
|
||||
[<count>] Number of packets to send
|
||||
$ ping 1.2.3.4 5
|
||||
Would ping: 1.2.3.4 with timeout 5s and count 5
|
||||
```
|
||||
|
||||
From the following source:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gopkg.in/alecthomas/kingpin.v2"
|
||||
)
|
||||
|
||||
var (
|
||||
debug = kingpin.Flag("debug", "Enable debug mode.").Bool()
|
||||
timeout = kingpin.Flag("timeout", "Timeout waiting for ping.").Default("5s").OverrideDefaultFromEnvar("PING_TIMEOUT").Short('t').Duration()
|
||||
ip = kingpin.Arg("ip", "IP address to ping.").Required().IP()
|
||||
count = kingpin.Arg("count", "Number of packets to send").Int()
|
||||
)
|
||||
|
||||
func main() {
|
||||
kingpin.Version("0.0.1")
|
||||
kingpin.Parse()
|
||||
fmt.Printf("Would ping: %s with timeout %s and count %d\n", *ip, *timeout, *count)
|
||||
}
|
||||
```
|
||||
|
||||
### Complex Example
|
||||
|
||||
Kingpin can also produce complex command-line applications with global flags,
|
||||
subcommands, and per-subcommand flags, like this:
|
||||
|
||||
```
|
||||
$ chat --help
|
||||
usage: chat [<flags>] <command> [<flags>] [<args> ...]
|
||||
|
||||
A command-line chat application.
|
||||
|
||||
Flags:
|
||||
--help Show help.
|
||||
--debug Enable debug mode.
|
||||
--server=127.0.0.1 Server address.
|
||||
|
||||
Commands:
|
||||
help [<command>]
|
||||
Show help for a command.
|
||||
|
||||
register <nick> <name>
|
||||
Register a new user.
|
||||
|
||||
post [<flags>] <channel> [<text>]
|
||||
Post a message to a channel.
|
||||
|
||||
$ chat help post
|
||||
usage: chat [<flags>] post [<flags>] <channel> [<text>]
|
||||
|
||||
Post a message to a channel.
|
||||
|
||||
Flags:
|
||||
--image=IMAGE Image to post.
|
||||
|
||||
Args:
|
||||
<channel> Channel to post to.
|
||||
[<text>] Text to post.
|
||||
|
||||
$ chat post --image=~/Downloads/owls.jpg pics
|
||||
...
|
||||
```
|
||||
|
||||
From this code:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"gopkg.in/alecthomas/kingpin.v2"
|
||||
)
|
||||
|
||||
var (
|
||||
app = kingpin.New("chat", "A command-line chat application.")
|
||||
debug = app.Flag("debug", "Enable debug mode.").Bool()
|
||||
serverIP = app.Flag("server", "Server address.").Default("127.0.0.1").IP()
|
||||
|
||||
register = app.Command("register", "Register a new user.")
|
||||
registerNick = register.Arg("nick", "Nickname for user.").Required().String()
|
||||
registerName = register.Arg("name", "Name of user.").Required().String()
|
||||
|
||||
post = app.Command("post", "Post a message to a channel.")
|
||||
postImage = post.Flag("image", "Image to post.").File()
|
||||
postChannel = post.Arg("channel", "Channel to post to.").Required().String()
|
||||
postText = post.Arg("text", "Text to post.").Strings()
|
||||
)
|
||||
|
||||
func main() {
|
||||
switch kingpin.MustParse(app.Parse(os.Args[1:])) {
|
||||
// Register user
|
||||
case register.FullCommand():
|
||||
println(*registerNick)
|
||||
|
||||
// Post message
|
||||
case post.FullCommand():
|
||||
if *postImage != nil {
|
||||
}
|
||||
text := strings.Join(*postText, " ")
|
||||
println("Post:", text)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Reference Documentation
|
||||
|
||||
### Displaying errors and usage information
|
||||
|
||||
Kingpin exports a set of functions to provide consistent errors and usage
|
||||
information to the user.
|
||||
|
||||
Error messages look something like this:
|
||||
|
||||
<app>: error: <message>
|
||||
|
||||
The functions on `Application` are:
|
||||
|
||||
Function | Purpose
|
||||
---------|--------------
|
||||
`Errorf(format, args)` | Display a printf formatted error to the user.
|
||||
`Fatalf(format, args)` | As with Errorf, but also call the termination handler.
|
||||
`FatalUsage(format, args)` | As with Fatalf, but also print contextual usage information.
|
||||
`FatalUsageContext(context, format, args)` | As with Fatalf, but also print contextual usage information from a `ParseContext`.
|
||||
`FatalIfError(err, format, args)` | Conditionally print an error prefixed with format+args, then call the termination handler
|
||||
|
||||
There are equivalent global functions in the kingpin namespace for the default
|
||||
`kingpin.CommandLine` instance.
|
||||
|
||||
### Sub-commands
|
||||
|
||||
Kingpin supports nested sub-commands, with separate flag and positional
|
||||
arguments per sub-command. Note that positional arguments may only occur after
|
||||
sub-commands.
|
||||
|
||||
For example:
|
||||
|
||||
```go
|
||||
var (
|
||||
deleteCommand = kingpin.Command("delete", "Delete an object.")
|
||||
deleteUserCommand = deleteCommand.Command("user", "Delete a user.")
|
||||
deleteUserUIDFlag = deleteUserCommand.Flag("uid", "Delete user by UID rather than username.")
|
||||
deleteUserUsername = deleteUserCommand.Arg("username", "Username to delete.")
|
||||
deletePostCommand = deleteCommand.Command("post", "Delete a post.")
|
||||
)
|
||||
|
||||
func main() {
|
||||
switch kingpin.Parse() {
|
||||
case "delete user":
|
||||
case "delete post":
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Parsers
|
||||
|
||||
Kingpin supports both flag and positional argument parsers for converting to
|
||||
Go types. For example, some included parsers are `Int()`, `Float()`,
|
||||
`Duration()` and `ExistingFile()` (see [parsers.go](./parsers.go) for a complete list of included parsers).
|
||||
|
||||
Parsers conform to Go's [`flag.Value`](http://godoc.org/flag#Value)
|
||||
interface, so any existing implementations will work.
|
||||
|
||||
For example, a parser for accumulating HTTP header values might look like this:
|
||||
|
||||
```go
|
||||
type HTTPHeaderValue http.Header
|
||||
|
||||
func (h *HTTPHeaderValue) Set(value string) error {
|
||||
parts := strings.SplitN(value, ":", 2)
|
||||
if len(parts) != 2 {
|
||||
return fmt.Errorf("expected HEADER:VALUE got '%s'", value)
|
||||
}
|
||||
(*http.Header)(h).Add(parts[0], parts[1])
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *HTTPHeaderValue) String() string {
|
||||
return ""
|
||||
}
|
||||
```
|
||||
|
||||
As a convenience, I would recommend something like this:
|
||||
|
||||
```go
|
||||
func HTTPHeader(s Settings) (target *http.Header) {
|
||||
target = &http.Header{}
|
||||
s.SetValue((*HTTPHeaderValue)(target))
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
You would use it like so:
|
||||
|
||||
```go
|
||||
headers = HTTPHeader(kingpin.Flag("header", "Add a HTTP header to the request.").Short('H'))
|
||||
```
|
||||
|
||||
### Repeatable flags
|
||||
|
||||
Depending on the `Value` they hold, some flags may be repeated. The
|
||||
`IsCumulative() bool` function on `Value` tells if it's safe to call `Set()`
|
||||
multiple times or if an error should be raised if several values are passed.
|
||||
|
||||
The built-in `Value`s returning slices and maps, as well as `Counter` are
|
||||
examples of `Value`s that make a flag repeatable.
|
||||
|
||||
### Boolean values
|
||||
|
||||
Boolean values are uniquely managed by Kingpin. Each boolean flag will have a negative complement:
|
||||
`--<name>` and `--no-<name>`.
|
||||
|
||||
### Default Values
|
||||
|
||||
The default value is the zero value for a type. This can be overridden with
|
||||
the `Default(value...)` function on flags and arguments. This function accepts
|
||||
one or several strings, which are parsed by the value itself, so they *must*
|
||||
be compliant with the format expected.
|
||||
|
||||
### Place-holders in Help
|
||||
|
||||
The place-holder value for a flag is the value used in the help to describe
|
||||
the value of a non-boolean flag.
|
||||
|
||||
The value provided to PlaceHolder() is used if provided, then the value
|
||||
provided by Default() if provided, then finally the capitalised flag name is
|
||||
used.
|
||||
|
||||
Here are some examples of flags with various permutations:
|
||||
|
||||
--name=NAME // Flag(...).String()
|
||||
--name="Harry" // Flag(...).Default("Harry").String()
|
||||
--name=FULL-NAME // Flag(...).PlaceHolder("FULL-NAME").Default("Harry").String()
|
||||
|
||||
### Consuming all remaining arguments
|
||||
|
||||
A common command-line idiom is to use all remaining arguments for some
|
||||
purpose. eg. The following command accepts an arbitrary number of
|
||||
IP addresses as positional arguments:
|
||||
|
||||
./cmd ping 10.1.1.1 192.168.1.1
|
||||
|
||||
Such arguments are similar to [repeatable flags](#repeatable-flags), but for
|
||||
arguments. Therefore they use the same `IsCumulative() bool` function on the
|
||||
underlying `Value`, so the built-in `Value`s for which the `Set()` function
|
||||
can be called several times will consume multiple arguments.
|
||||
|
||||
To implement the above example with a custom `Value`, we might do something
|
||||
like this:
|
||||
|
||||
```go
|
||||
type ipList []net.IP
|
||||
|
||||
func (i *ipList) Set(value string) error {
|
||||
if ip := net.ParseIP(value); ip == nil {
|
||||
return fmt.Errorf("'%s' is not an IP address", value)
|
||||
} else {
|
||||
*i = append(*i, ip)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (i *ipList) String() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (i *ipList) IsCumulative() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func IPList(s Settings) (target *[]net.IP) {
|
||||
target = new([]net.IP)
|
||||
s.SetValue((*ipList)(target))
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
And use it like so:
|
||||
|
||||
```go
|
||||
ips := IPList(kingpin.Arg("ips", "IP addresses to ping."))
|
||||
```
|
||||
|
||||
### Bash/ZSH Shell Completion
|
||||
|
||||
By default, all flags and commands/subcommands generate completions
|
||||
internally.
|
||||
|
||||
Out of the box, CLI tools using kingpin should be able to take advantage
|
||||
of completion hinting for flags and commands. By specifying
|
||||
`--completion-bash` as the first argument, your CLI tool will show
|
||||
possible subcommands. By ending your argv with `--`, hints for flags
|
||||
will be shown.
|
||||
|
||||
To allow your end users to take advantage you must package a
|
||||
`/etc/bash_completion.d` script with your distribution (or the equivalent
|
||||
for your target platform/shell). An alternative is to instruct your end
|
||||
user to source a script from their `bash_profile` (or equivalent).
|
||||
|
||||
Fortunately Kingpin makes it easy to generate or source a script for use
|
||||
with end users shells. `./yourtool --completion-script-bash` and
|
||||
`./yourtool --completion-script-zsh` will generate these scripts for you.
|
||||
|
||||
**Installation by Package**
|
||||
|
||||
For the best user experience, you should bundle your pre-created
|
||||
completion script with your CLI tool and install it inside
|
||||
`/etc/bash_completion.d` (or equivalent). A good suggestion is to add
|
||||
this as an automated step to your build pipeline, in the implementation
|
||||
is improved for bug fixed.
|
||||
|
||||
**Installation by `bash_profile`**
|
||||
|
||||
Alternatively, instruct your users to add an additional statement to
|
||||
their `bash_profile` (or equivalent):
|
||||
|
||||
```
|
||||
eval "$(your-cli-tool --completion-script-bash)"
|
||||
```
|
||||
|
||||
Or for ZSH
|
||||
|
||||
```
|
||||
eval "$(your-cli-tool --completion-script-zsh)"
|
||||
```
|
||||
|
||||
#### Additional API
|
||||
To provide more flexibility, a completion option API has been
|
||||
exposed for flags to allow user defined completion options, to extend
|
||||
completions further than just EnumVar/Enum.
|
||||
|
||||
|
||||
**Provide Static Options**
|
||||
|
||||
When using an `Enum` or `EnumVar`, users are limited to only the options
|
||||
given. Maybe we wish to hint possible options to the user, but also
|
||||
allow them to provide their own custom option. `HintOptions` gives
|
||||
this functionality to flags.
|
||||
|
||||
```
|
||||
app := kingpin.New("completion", "My application with bash completion.")
|
||||
app.Flag("port", "Provide a port to connect to").
|
||||
Required().
|
||||
HintOptions("80", "443", "8080").
|
||||
IntVar(&c.port)
|
||||
```
|
||||
|
||||
**Provide Dynamic Options**
|
||||
Consider the case that you needed to read a local database or a file to
|
||||
provide suggestions. You can dynamically generate the options
|
||||
|
||||
```
|
||||
func listHosts() []string {
|
||||
// Provide a dynamic list of hosts from a hosts file or otherwise
|
||||
// for bash completion. In this example we simply return static slice.
|
||||
|
||||
// You could use this functionality to reach into a hosts file to provide
|
||||
// completion for a list of known hosts.
|
||||
return []string{"sshhost.example", "webhost.example", "ftphost.example"}
|
||||
}
|
||||
|
||||
app := kingpin.New("completion", "My application with bash completion.")
|
||||
app.Flag("flag-1", "").HintAction(listHosts).String()
|
||||
```
|
||||
|
||||
**EnumVar/Enum**
|
||||
When using `Enum` or `EnumVar`, any provided options will be automatically
|
||||
used for bash autocompletion. However, if you wish to provide a subset or
|
||||
different options, you can use `HintOptions` or `HintAction` which will override
|
||||
the default completion options for `Enum`/`EnumVar`.
|
||||
|
||||
|
||||
**Examples**
|
||||
You can see an in depth example of the completion API within
|
||||
`examples/completion/main.go`
|
||||
|
||||
|
||||
### Supporting -h for help
|
||||
|
||||
`kingpin.CommandLine.HelpFlag.Short('h')`
|
||||
|
||||
### Custom help
|
||||
|
||||
Kingpin v2 supports templatised help using the text/template library (actually, [a fork](https://github.com/alecthomas/template)).
|
||||
|
||||
You can specify the template to use with the [Application.UsageTemplate()](http://godoc.org/gopkg.in/alecthomas/kingpin.v2#Application.UsageTemplate) function.
|
||||
|
||||
There are four included templates: `kingpin.DefaultUsageTemplate` is the default,
|
||||
`kingpin.CompactUsageTemplate` provides a more compact representation for more complex command-line structures,
|
||||
`kingpin.SeparateOptionalFlagsUsageTemplate` looks like the default template, but splits required
|
||||
and optional command flags into separate lists, and `kingpin.ManPageTemplate` is used to generate man pages.
|
||||
|
||||
See the above templates for examples of usage, and the the function [UsageForContextWithTemplate()](https://github.com/alecthomas/kingpin/blob/master/usage.go#L198) method for details on the context.
|
||||
|
||||
#### Default help template
|
||||
|
||||
```
|
||||
$ go run ./examples/curl/curl.go --help
|
||||
usage: curl [<flags>] <command> [<args> ...]
|
||||
|
||||
An example implementation of curl.
|
||||
|
||||
Flags:
|
||||
--help Show help.
|
||||
-t, --timeout=5s Set connection timeout.
|
||||
-H, --headers=HEADER=VALUE
|
||||
Add HTTP headers to the request.
|
||||
|
||||
Commands:
|
||||
help [<command>...]
|
||||
Show help.
|
||||
|
||||
get url <url>
|
||||
Retrieve a URL.
|
||||
|
||||
get file <file>
|
||||
Retrieve a file.
|
||||
|
||||
post [<flags>] <url>
|
||||
POST a resource.
|
||||
```
|
||||
|
||||
#### Compact help template
|
||||
|
||||
```
|
||||
$ go run ./examples/curl/curl.go --help
|
||||
usage: curl [<flags>] <command> [<args> ...]
|
||||
|
||||
An example implementation of curl.
|
||||
|
||||
Flags:
|
||||
--help Show help.
|
||||
-t, --timeout=5s Set connection timeout.
|
||||
-H, --headers=HEADER=VALUE
|
||||
Add HTTP headers to the request.
|
||||
|
||||
Commands:
|
||||
help [<command>...]
|
||||
get [<flags>]
|
||||
url <url>
|
||||
file <file>
|
||||
post [<flags>] <url>
|
||||
```
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
package kingpin
|
||||
|
||||
// Action callback executed at various stages after all values are populated.
|
||||
// The application, commands, arguments and flags all have corresponding
|
||||
// actions.
|
||||
type Action func(*ParseContext) error
|
||||
|
||||
type actionMixin struct {
|
||||
actions []Action
|
||||
preActions []Action
|
||||
}
|
||||
|
||||
type actionApplier interface {
|
||||
applyActions(*ParseContext) error
|
||||
applyPreActions(*ParseContext) error
|
||||
}
|
||||
|
||||
func (a *actionMixin) addAction(action Action) {
|
||||
a.actions = append(a.actions, action)
|
||||
}
|
||||
|
||||
func (a *actionMixin) addPreAction(action Action) {
|
||||
a.preActions = append(a.preActions, action)
|
||||
}
|
||||
|
||||
func (a *actionMixin) applyActions(context *ParseContext) error {
|
||||
for _, action := range a.actions {
|
||||
if err := action(context); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *actionMixin) applyPreActions(context *ParseContext) error {
|
||||
for _, preAction := range a.preActions {
|
||||
if err := preAction(context); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
-688
@@ -1,688 +0,0 @@
|
||||
package kingpin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrCommandNotSpecified = fmt.Errorf("command not specified")
|
||||
)
|
||||
|
||||
var (
|
||||
envarTransformRegexp = regexp.MustCompile(`[^a-zA-Z0-9_]+`)
|
||||
)
|
||||
|
||||
type ApplicationValidator func(*Application) error
|
||||
|
||||
// An Application contains the definitions of flags, arguments and commands
|
||||
// for an application.
|
||||
type Application struct {
|
||||
cmdMixin
|
||||
initialized bool
|
||||
|
||||
Name string
|
||||
Help string
|
||||
|
||||
author string
|
||||
version string
|
||||
errorWriter io.Writer // Destination for errors.
|
||||
usageWriter io.Writer // Destination for usage
|
||||
usageTemplate string
|
||||
validator ApplicationValidator
|
||||
terminate func(status int) // See Terminate()
|
||||
noInterspersed bool // can flags be interspersed with args (or must they come first)
|
||||
defaultEnvars bool
|
||||
completion bool
|
||||
|
||||
// Help flag. Exposed for user customisation.
|
||||
HelpFlag *FlagClause
|
||||
// Help command. Exposed for user customisation. May be nil.
|
||||
HelpCommand *CmdClause
|
||||
// Version flag. Exposed for user customisation. May be nil.
|
||||
VersionFlag *FlagClause
|
||||
}
|
||||
|
||||
// New creates a new Kingpin application instance.
|
||||
func New(name, help string) *Application {
|
||||
a := &Application{
|
||||
Name: name,
|
||||
Help: help,
|
||||
errorWriter: os.Stderr, // Left for backwards compatibility purposes.
|
||||
usageWriter: os.Stderr,
|
||||
usageTemplate: DefaultUsageTemplate,
|
||||
terminate: os.Exit,
|
||||
}
|
||||
a.flagGroup = newFlagGroup()
|
||||
a.argGroup = newArgGroup()
|
||||
a.cmdGroup = newCmdGroup(a)
|
||||
a.HelpFlag = a.Flag("help", "Show context-sensitive help (also try --help-long and --help-man).")
|
||||
a.HelpFlag.Bool()
|
||||
a.Flag("help-long", "Generate long help.").Hidden().PreAction(a.generateLongHelp).Bool()
|
||||
a.Flag("help-man", "Generate a man page.").Hidden().PreAction(a.generateManPage).Bool()
|
||||
a.Flag("completion-bash", "Output possible completions for the given args.").Hidden().BoolVar(&a.completion)
|
||||
a.Flag("completion-script-bash", "Generate completion script for bash.").Hidden().PreAction(a.generateBashCompletionScript).Bool()
|
||||
a.Flag("completion-script-zsh", "Generate completion script for ZSH.").Hidden().PreAction(a.generateZSHCompletionScript).Bool()
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *Application) generateLongHelp(c *ParseContext) error {
|
||||
a.Writer(os.Stdout)
|
||||
if err := a.UsageForContextWithTemplate(c, 2, LongHelpTemplate); err != nil {
|
||||
return err
|
||||
}
|
||||
a.terminate(0)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Application) generateManPage(c *ParseContext) error {
|
||||
a.Writer(os.Stdout)
|
||||
if err := a.UsageForContextWithTemplate(c, 2, ManPageTemplate); err != nil {
|
||||
return err
|
||||
}
|
||||
a.terminate(0)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Application) generateBashCompletionScript(c *ParseContext) error {
|
||||
a.Writer(os.Stdout)
|
||||
if err := a.UsageForContextWithTemplate(c, 2, BashCompletionTemplate); err != nil {
|
||||
return err
|
||||
}
|
||||
a.terminate(0)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Application) generateZSHCompletionScript(c *ParseContext) error {
|
||||
a.Writer(os.Stdout)
|
||||
if err := a.UsageForContextWithTemplate(c, 2, ZshCompletionTemplate); err != nil {
|
||||
return err
|
||||
}
|
||||
a.terminate(0)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DefaultEnvars configures all flags (that do not already have an associated
|
||||
// envar) to use a default environment variable in the form "<app>_<flag>".
|
||||
//
|
||||
// For example, if the application is named "foo" and a flag is named "bar-
|
||||
// waz" the environment variable: "FOO_BAR_WAZ".
|
||||
func (a *Application) DefaultEnvars() *Application {
|
||||
a.defaultEnvars = true
|
||||
return a
|
||||
}
|
||||
|
||||
// Terminate specifies the termination handler. Defaults to os.Exit(status).
|
||||
// If nil is passed, a no-op function will be used.
|
||||
func (a *Application) Terminate(terminate func(int)) *Application {
|
||||
if terminate == nil {
|
||||
terminate = func(int) {}
|
||||
}
|
||||
a.terminate = terminate
|
||||
return a
|
||||
}
|
||||
|
||||
// Writer specifies the writer to use for usage and errors. Defaults to os.Stderr.
|
||||
// DEPRECATED: See ErrorWriter and UsageWriter.
|
||||
func (a *Application) Writer(w io.Writer) *Application {
|
||||
a.errorWriter = w
|
||||
a.usageWriter = w
|
||||
return a
|
||||
}
|
||||
|
||||
// ErrorWriter sets the io.Writer to use for errors.
|
||||
func (a *Application) ErrorWriter(w io.Writer) *Application {
|
||||
a.errorWriter = w
|
||||
return a
|
||||
}
|
||||
|
||||
// UsageWriter sets the io.Writer to use for errors.
|
||||
func (a *Application) UsageWriter(w io.Writer) *Application {
|
||||
a.usageWriter = w
|
||||
return a
|
||||
}
|
||||
|
||||
// UsageTemplate specifies the text template to use when displaying usage
|
||||
// information. The default is UsageTemplate.
|
||||
func (a *Application) UsageTemplate(template string) *Application {
|
||||
a.usageTemplate = template
|
||||
return a
|
||||
}
|
||||
|
||||
// Validate sets a validation function to run when parsing.
|
||||
func (a *Application) Validate(validator ApplicationValidator) *Application {
|
||||
a.validator = validator
|
||||
return a
|
||||
}
|
||||
|
||||
// ParseContext parses the given command line and returns the fully populated
|
||||
// ParseContext.
|
||||
func (a *Application) ParseContext(args []string) (*ParseContext, error) {
|
||||
return a.parseContext(false, args)
|
||||
}
|
||||
|
||||
func (a *Application) parseContext(ignoreDefault bool, args []string) (*ParseContext, error) {
|
||||
if err := a.init(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
context := tokenize(args, ignoreDefault)
|
||||
err := parse(context, a)
|
||||
return context, err
|
||||
}
|
||||
|
||||
// Parse parses command-line arguments. It returns the selected command and an
|
||||
// error. The selected command will be a space separated subcommand, if
|
||||
// subcommands have been configured.
|
||||
//
|
||||
// This will populate all flag and argument values, call all callbacks, and so
|
||||
// on.
|
||||
func (a *Application) Parse(args []string) (command string, err error) {
|
||||
|
||||
context, parseErr := a.ParseContext(args)
|
||||
selected := []string{}
|
||||
var setValuesErr error
|
||||
|
||||
if context == nil {
|
||||
// Since we do not throw error immediately, there could be a case
|
||||
// where a context returns nil. Protect against that.
|
||||
return "", parseErr
|
||||
}
|
||||
|
||||
if err = a.setDefaults(context); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
selected, setValuesErr = a.setValues(context)
|
||||
|
||||
if err = a.applyPreActions(context, !a.completion); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if a.completion {
|
||||
a.generateBashCompletion(context)
|
||||
a.terminate(0)
|
||||
} else {
|
||||
if parseErr != nil {
|
||||
return "", parseErr
|
||||
}
|
||||
|
||||
a.maybeHelp(context)
|
||||
if !context.EOL() {
|
||||
return "", fmt.Errorf("unexpected argument '%s'", context.Peek())
|
||||
}
|
||||
|
||||
if setValuesErr != nil {
|
||||
return "", setValuesErr
|
||||
}
|
||||
|
||||
command, err = a.execute(context, selected)
|
||||
if err == ErrCommandNotSpecified {
|
||||
a.writeUsage(context, nil)
|
||||
}
|
||||
}
|
||||
return command, err
|
||||
}
|
||||
|
||||
func (a *Application) writeUsage(context *ParseContext, err error) {
|
||||
if err != nil {
|
||||
a.Errorf("%s", err)
|
||||
}
|
||||
if err := a.UsageForContext(context); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err != nil {
|
||||
a.terminate(1)
|
||||
} else {
|
||||
a.terminate(0)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Application) maybeHelp(context *ParseContext) {
|
||||
for _, element := range context.Elements {
|
||||
if flag, ok := element.Clause.(*FlagClause); ok && flag == a.HelpFlag {
|
||||
// Re-parse the command-line ignoring defaults, so that help works correctly.
|
||||
context, _ = a.parseContext(true, context.rawArgs)
|
||||
a.writeUsage(context, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Version adds a --version flag for displaying the application version.
|
||||
func (a *Application) Version(version string) *Application {
|
||||
a.version = version
|
||||
a.VersionFlag = a.Flag("version", "Show application version.").PreAction(func(*ParseContext) error {
|
||||
fmt.Fprintln(a.usageWriter, version)
|
||||
a.terminate(0)
|
||||
return nil
|
||||
})
|
||||
a.VersionFlag.Bool()
|
||||
return a
|
||||
}
|
||||
|
||||
// Author sets the author output by some help templates.
|
||||
func (a *Application) Author(author string) *Application {
|
||||
a.author = author
|
||||
return a
|
||||
}
|
||||
|
||||
// Action callback to call when all values are populated and parsing is
|
||||
// complete, but before any command, flag or argument actions.
|
||||
//
|
||||
// All Action() callbacks are called in the order they are encountered on the
|
||||
// command line.
|
||||
func (a *Application) Action(action Action) *Application {
|
||||
a.addAction(action)
|
||||
return a
|
||||
}
|
||||
|
||||
// Action called after parsing completes but before validation and execution.
|
||||
func (a *Application) PreAction(action Action) *Application {
|
||||
a.addPreAction(action)
|
||||
return a
|
||||
}
|
||||
|
||||
// Command adds a new top-level command.
|
||||
func (a *Application) Command(name, help string) *CmdClause {
|
||||
return a.addCommand(name, help)
|
||||
}
|
||||
|
||||
// Interspersed control if flags can be interspersed with positional arguments
|
||||
//
|
||||
// true (the default) means that they can, false means that all the flags must appear before the first positional arguments.
|
||||
func (a *Application) Interspersed(interspersed bool) *Application {
|
||||
a.noInterspersed = !interspersed
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *Application) defaultEnvarPrefix() string {
|
||||
if a.defaultEnvars {
|
||||
return a.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (a *Application) init() error {
|
||||
if a.initialized {
|
||||
return nil
|
||||
}
|
||||
if a.cmdGroup.have() && a.argGroup.have() {
|
||||
return fmt.Errorf("can't mix top-level Arg()s with Command()s")
|
||||
}
|
||||
|
||||
// If we have subcommands, add a help command at the top-level.
|
||||
if a.cmdGroup.have() {
|
||||
var command []string
|
||||
a.HelpCommand = a.Command("help", "Show help.").PreAction(func(context *ParseContext) error {
|
||||
a.Usage(command)
|
||||
a.terminate(0)
|
||||
return nil
|
||||
})
|
||||
a.HelpCommand.Arg("command", "Show help on command.").StringsVar(&command)
|
||||
// Make help first command.
|
||||
l := len(a.commandOrder)
|
||||
a.commandOrder = append(a.commandOrder[l-1:l], a.commandOrder[:l-1]...)
|
||||
}
|
||||
|
||||
if err := a.flagGroup.init(a.defaultEnvarPrefix()); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := a.cmdGroup.init(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := a.argGroup.init(); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, cmd := range a.commands {
|
||||
if err := cmd.init(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
flagGroups := []*flagGroup{a.flagGroup}
|
||||
for _, cmd := range a.commandOrder {
|
||||
if err := checkDuplicateFlags(cmd, flagGroups); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
a.initialized = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Recursively check commands for duplicate flags.
|
||||
func checkDuplicateFlags(current *CmdClause, flagGroups []*flagGroup) error {
|
||||
// Check for duplicates.
|
||||
for _, flags := range flagGroups {
|
||||
for _, flag := range current.flagOrder {
|
||||
if flag.shorthand != 0 {
|
||||
if _, ok := flags.short[string(flag.shorthand)]; ok {
|
||||
return fmt.Errorf("duplicate short flag -%c", flag.shorthand)
|
||||
}
|
||||
}
|
||||
if _, ok := flags.long[flag.name]; ok {
|
||||
return fmt.Errorf("duplicate long flag --%s", flag.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
flagGroups = append(flagGroups, current.flagGroup)
|
||||
// Check subcommands.
|
||||
for _, subcmd := range current.commandOrder {
|
||||
if err := checkDuplicateFlags(subcmd, flagGroups); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Application) execute(context *ParseContext, selected []string) (string, error) {
|
||||
var err error
|
||||
|
||||
if err = a.validateRequired(context); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err = a.applyValidators(context); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err = a.applyActions(context); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
command := strings.Join(selected, " ")
|
||||
if command == "" && a.cmdGroup.have() {
|
||||
return "", ErrCommandNotSpecified
|
||||
}
|
||||
return command, err
|
||||
}
|
||||
|
||||
func (a *Application) setDefaults(context *ParseContext) error {
|
||||
flagElements := map[string]*ParseElement{}
|
||||
for _, element := range context.Elements {
|
||||
if flag, ok := element.Clause.(*FlagClause); ok {
|
||||
if flag.name == "help" {
|
||||
return nil
|
||||
}
|
||||
flagElements[flag.name] = element
|
||||
}
|
||||
}
|
||||
|
||||
argElements := map[string]*ParseElement{}
|
||||
for _, element := range context.Elements {
|
||||
if arg, ok := element.Clause.(*ArgClause); ok {
|
||||
argElements[arg.name] = element
|
||||
}
|
||||
}
|
||||
|
||||
// Check required flags and set defaults.
|
||||
for _, flag := range context.flags.long {
|
||||
if flagElements[flag.name] == nil {
|
||||
if err := flag.setDefault(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, arg := range context.arguments.args {
|
||||
if argElements[arg.name] == nil {
|
||||
if err := arg.setDefault(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Application) validateRequired(context *ParseContext) error {
|
||||
flagElements := map[string]*ParseElement{}
|
||||
for _, element := range context.Elements {
|
||||
if flag, ok := element.Clause.(*FlagClause); ok {
|
||||
flagElements[flag.name] = element
|
||||
}
|
||||
}
|
||||
|
||||
argElements := map[string]*ParseElement{}
|
||||
for _, element := range context.Elements {
|
||||
if arg, ok := element.Clause.(*ArgClause); ok {
|
||||
argElements[arg.name] = element
|
||||
}
|
||||
}
|
||||
|
||||
// Check required flags and set defaults.
|
||||
for _, flag := range context.flags.long {
|
||||
if flagElements[flag.name] == nil {
|
||||
// Check required flags were provided.
|
||||
if flag.needsValue() {
|
||||
return fmt.Errorf("required flag --%s not provided", flag.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, arg := range context.arguments.args {
|
||||
if argElements[arg.name] == nil {
|
||||
if arg.needsValue() {
|
||||
return fmt.Errorf("required argument '%s' not provided", arg.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Application) setValues(context *ParseContext) (selected []string, err error) {
|
||||
// Set all arg and flag values.
|
||||
var (
|
||||
lastCmd *CmdClause
|
||||
flagSet = map[string]struct{}{}
|
||||
)
|
||||
for _, element := range context.Elements {
|
||||
switch clause := element.Clause.(type) {
|
||||
case *FlagClause:
|
||||
if _, ok := flagSet[clause.name]; ok {
|
||||
if v, ok := clause.value.(repeatableFlag); !ok || !v.IsCumulative() {
|
||||
return nil, fmt.Errorf("flag '%s' cannot be repeated", clause.name)
|
||||
}
|
||||
}
|
||||
if err = clause.value.Set(*element.Value); err != nil {
|
||||
return
|
||||
}
|
||||
flagSet[clause.name] = struct{}{}
|
||||
|
||||
case *ArgClause:
|
||||
if err = clause.value.Set(*element.Value); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
case *CmdClause:
|
||||
if clause.validator != nil {
|
||||
if err = clause.validator(clause); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
selected = append(selected, clause.name)
|
||||
lastCmd = clause
|
||||
}
|
||||
}
|
||||
|
||||
if lastCmd != nil && len(lastCmd.commands) > 0 {
|
||||
return nil, fmt.Errorf("must select a subcommand of '%s'", lastCmd.FullCommand())
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (a *Application) applyValidators(context *ParseContext) (err error) {
|
||||
// Call command validation functions.
|
||||
for _, element := range context.Elements {
|
||||
if cmd, ok := element.Clause.(*CmdClause); ok && cmd.validator != nil {
|
||||
if err = cmd.validator(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if a.validator != nil {
|
||||
err = a.validator(a)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *Application) applyPreActions(context *ParseContext, dispatch bool) error {
|
||||
if err := a.actionMixin.applyPreActions(context); err != nil {
|
||||
return err
|
||||
}
|
||||
// Dispatch to actions.
|
||||
if dispatch {
|
||||
for _, element := range context.Elements {
|
||||
if applier, ok := element.Clause.(actionApplier); ok {
|
||||
if err := applier.applyPreActions(context); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Application) applyActions(context *ParseContext) error {
|
||||
if err := a.actionMixin.applyActions(context); err != nil {
|
||||
return err
|
||||
}
|
||||
// Dispatch to actions.
|
||||
for _, element := range context.Elements {
|
||||
if applier, ok := element.Clause.(actionApplier); ok {
|
||||
if err := applier.applyActions(context); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Errorf prints an error message to w in the format "<appname>: error: <message>".
|
||||
func (a *Application) Errorf(format string, args ...interface{}) {
|
||||
fmt.Fprintf(a.errorWriter, a.Name+": error: "+format+"\n", args...)
|
||||
}
|
||||
|
||||
// Fatalf writes a formatted error to w then terminates with exit status 1.
|
||||
func (a *Application) Fatalf(format string, args ...interface{}) {
|
||||
a.Errorf(format, args...)
|
||||
a.terminate(1)
|
||||
}
|
||||
|
||||
// FatalUsage prints an error message followed by usage information, then
|
||||
// exits with a non-zero status.
|
||||
func (a *Application) FatalUsage(format string, args ...interface{}) {
|
||||
a.Errorf(format, args...)
|
||||
// Force usage to go to error output.
|
||||
a.usageWriter = a.errorWriter
|
||||
a.Usage([]string{})
|
||||
a.terminate(1)
|
||||
}
|
||||
|
||||
// FatalUsageContext writes a printf formatted error message to w, then usage
|
||||
// information for the given ParseContext, before exiting.
|
||||
func (a *Application) FatalUsageContext(context *ParseContext, format string, args ...interface{}) {
|
||||
a.Errorf(format, args...)
|
||||
if err := a.UsageForContext(context); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
a.terminate(1)
|
||||
}
|
||||
|
||||
// FatalIfError prints an error and exits if err is not nil. The error is printed
|
||||
// with the given formatted string, if any.
|
||||
func (a *Application) FatalIfError(err error, format string, args ...interface{}) {
|
||||
if err != nil {
|
||||
prefix := ""
|
||||
if format != "" {
|
||||
prefix = fmt.Sprintf(format, args...) + ": "
|
||||
}
|
||||
a.Errorf(prefix+"%s", err)
|
||||
a.terminate(1)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Application) completionOptions(context *ParseContext) []string {
|
||||
args := context.rawArgs
|
||||
|
||||
var (
|
||||
currArg string
|
||||
prevArg string
|
||||
target cmdMixin
|
||||
)
|
||||
|
||||
numArgs := len(args)
|
||||
if numArgs > 1 {
|
||||
args = args[1:]
|
||||
currArg = args[len(args)-1]
|
||||
}
|
||||
if numArgs > 2 {
|
||||
prevArg = args[len(args)-2]
|
||||
}
|
||||
|
||||
target = a.cmdMixin
|
||||
if context.SelectedCommand != nil {
|
||||
// A subcommand was in use. We will use it as the target
|
||||
target = context.SelectedCommand.cmdMixin
|
||||
}
|
||||
|
||||
if (currArg != "" && strings.HasPrefix(currArg, "--")) || strings.HasPrefix(prevArg, "--") {
|
||||
// Perform completion for A flag. The last/current argument started with "-"
|
||||
var (
|
||||
flagName string // The name of a flag if given (could be half complete)
|
||||
flagValue string // The value assigned to a flag (if given) (could be half complete)
|
||||
)
|
||||
|
||||
if strings.HasPrefix(prevArg, "--") && !strings.HasPrefix(currArg, "--") {
|
||||
// Matches: ./myApp --flag value
|
||||
// Wont Match: ./myApp --flag --
|
||||
flagName = prevArg[2:] // Strip the "--"
|
||||
flagValue = currArg
|
||||
} else if strings.HasPrefix(currArg, "--") {
|
||||
// Matches: ./myApp --flag --
|
||||
// Matches: ./myApp --flag somevalue --
|
||||
// Matches: ./myApp --
|
||||
flagName = currArg[2:] // Strip the "--"
|
||||
}
|
||||
|
||||
options, flagMatched, valueMatched := target.FlagCompletion(flagName, flagValue)
|
||||
if valueMatched {
|
||||
// Value Matched. Show cmdCompletions
|
||||
return target.CmdCompletion(context)
|
||||
}
|
||||
|
||||
// Add top level flags if we're not at the top level and no match was found.
|
||||
if context.SelectedCommand != nil && !flagMatched {
|
||||
topOptions, topFlagMatched, topValueMatched := a.FlagCompletion(flagName, flagValue)
|
||||
if topValueMatched {
|
||||
// Value Matched. Back to cmdCompletions
|
||||
return target.CmdCompletion(context)
|
||||
}
|
||||
|
||||
if topFlagMatched {
|
||||
// Top level had a flag which matched the input. Return it's options.
|
||||
options = topOptions
|
||||
} else {
|
||||
// Add top level flags
|
||||
options = append(options, topOptions...)
|
||||
}
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
// Perform completion for sub commands and arguments.
|
||||
return target.CmdCompletion(context)
|
||||
}
|
||||
|
||||
func (a *Application) generateBashCompletion(context *ParseContext) {
|
||||
options := a.completionOptions(context)
|
||||
fmt.Printf("%s", strings.Join(options, "\n"))
|
||||
}
|
||||
|
||||
func envarTransform(name string) string {
|
||||
return strings.ToUpper(envarTransformRegexp.ReplaceAllString(name, "_"))
|
||||
}
|
||||
Loaded 100 of 559 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user