1.0.0, Fixed #42, Fixed #41, Fixed #40, Fixed #39, Fixed #38, Fixed #36, Fixed #29, Fixed #17

This commit is contained in:
Yami Odymel 2024-01-24 01:23:06 +08:00
parent b72d565cfa
commit cf60326fd8
No known key found for this signature in database
GPG Key ID: 68E469836934DB36
33 changed files with 123 additions and 851 deletions

1
.old/.gitignore vendored
View File

@ -1 +0,0 @@
./video

View File

@ -1,7 +0,0 @@
GOOS=windows GOARCH=amd64 go build -o bin/windows/chatubrate-dvr.exe &&
GOOS=darwin GOARCH=amd64 go build -o bin/darwin/chatubrate-dvr &&
GOOS=linux GOARCH=amd64 go build -o bin/linux/chatubrate-dvr
GOOS=windows GOARCH=arm64 go build -o bin/arm64/windows/chatubrate-dvr.exe &&
GOOS=darwin GOARCH=arm64 go build -o bin/arm64/darwin/chatubrate-dvr &&
GOOS=linux GOARCH=arm64 go build -o bin/arm64/linux/chatubrate-dvr

View File

@ -1,11 +0,0 @@
FROM golang:latest
WORKDIR /usr/src/app
COPY go.mod go.sum ./
RUN go mod download && go mod verify
COPY . .
RUN go build
CMD [ "sh", "-c", "./chaturbate-dvr -u $USERNAME" ]

View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022 TeaCat
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.

View File

@ -1,90 +0,0 @@
# Chaturbate 重新錄播 (Alpha)
這個程式能夠監聽指定的 Chaturbate 頻道,並且在該頻道開始直播時自動儲存影片至本機。這樣你就不會錯過任何精彩的事情
**警告**:在 Chaturbate 上的直播內容都有版權,你不應該複製、分享、散播這些內容。(想閱讀更多,請參閱 [DMCA](https://www.dmca.com/)
**免責聲明**因為這還在早期開發階段錄播內容可能會有幀數遺失3 小時直播遺失 20 秒內容),這仍然需要測試。
## 使用方式
這個程式能夠在 64 位元的 macOS、Linux、Windows懶得編譯 32 位元的版本)上正常運作。你只需要進入 `/bin` 資料夾找到對應你的系統,然後在終端機執行該檔案即可。
```bash
$ chaturbate-dvr -u 好棒棒頻道名稱
.o88b. db db .d8b. d888888b db db d8888b. d8888b. .d8b. d888888b d88888b
d8P Y8 88 88 d8' `8b `~~88~~' 88 88 88 `8D 88 `8D d8' `8b `~~88~~' 88'
8P 88ooo88 88ooo88 88 88 88 88oobY' 88oooY' 88ooo88 88 88ooooo
8b 88~~~88 88~~~88 88 88 88 88`8b 88~~~b. 88~~~88 88 88~~~~~
Y8b d8 88 88 88 88 88 88b d88 88 `88. 88 8D 88 88 88 88.
`Y88P' YP YP YP YP YP ~Y8888P' 88 YD Y8888P' YP YP YP Y88888P
d8888b. db db d8888b.
88 `8D 88 88 88 `8D
88 88 Y8 8P 88oobY'
88 88 `8b d8' 88`8b
88 .8D `8bd8' 88 `88.
Y8888D' YP 88 YD
---
2020/02/13 18:05:22 好棒棒頻道名稱 is online! fetching...
2020/02/13 18:05:24 the video will be saved as "2020-02-13_22-16-27.ts".
2020/02/13 18:05:28 fetching media_w402018999_b5128000_t64RlBTOjI5Ljk3_9134.ts (size: 936428)
2020/02/13 19:07:06 failed to fetch the video segments, will try again. (1/2)
2020/02/13 19:07:06 failed to fetch the video segments, will try again. (2/2)
2020/02/13 19:07:11 failed to fetch the video segments after retried, 好棒棒頻道名稱 might went offline.
2020/02/13 19:07:11 好棒棒頻道名稱 is not online, check again after 3 minute(s)...
```
## 說明
```bash
NAME:
chaturbate-dvr - watching a specified chaturbate channel and auto saves the stream as local file
USAGE:
main [global options] command [command options] [arguments...]
COMMANDS:
help, h Shows a list of commands or help for one command
GLOBAL OPTIONS:
--username value, -u value channel username to watching
--interval value, -i value minutes to check if a channel goes online or not (default: 1)
--strip value, -s value MB sizes to split the video into chunks (default: 0)
--resolution 240, -r 240 Video resolution, could be 240, `480`, `540`, `720`, `1080` (default: "1080")
--resolution-fallback up, --rf up Looking for larger or smaller resolution (up for larger, `down` for smaller) if a specified resolution was not found (default: "down")
--fps value, -f value Preferred framerate, only works if streaming source supports it, otherwise it will always be 30 FPS (default: "60")
--help, -h show help (default: false)
--version, -v print the version (default: false)
```
## 中文對應
```
XXX is online! fetching...
XXX 正在線上!開始撈取實況內容…
the video will be saved as "XXX".
影片將會被保存為「XXX」。
fetching XXX.ts (size: XXX)
正在擷取 XXX.ts 片段大小XXX
failed to fetch the video segments, will try again. (1/2)
無法取得影片段落,稍後會重新嘗試。(1/2)
failed to fetch the video segments after retried, XXX might went offline.
無法取得影片段落XXX 可能已經結束直播了。
cannot find segment XXX, will try again. (1/5)
無法找到影片段落燒後會重新嘗試。1/5
inserting XXX segment to the master file. (total: XXX)
正在插入片段 XXX 至主要影片檔案。總共XXX
skipped XXX due to the empty body!
跳過 XXX 片段因為其為空白內容!
exceeded the specified stripping limit, creating new video file. (file: XXX)
達到影片分割上限建立新的影片檔案檔名XXX
```

View File

@ -1,61 +0,0 @@
# Chaturbate DVR (Alpha)
[[正體中文翻譯點此]](README-tw.md)
The program watches a specified Chaturbate channel and save the stream in real-time when the channel goes online, so you won't miss anything.
**Warning**: The streaming content on Chaturbate is copyrighted, you should not copy, share, distribute the content. (for more information, check [DMCA](https://www.dmca.com/))
**Disclaimer**: Due to early development, might have frames dropped (20s gone in a 3hrs long stream), still requires more tests.
## Usage
The program works for 64-bit macOS, Linux, Windows (too lazy to compile for 32-bit). Just get in the `/bin` folder and find your operating system then execute the program in terminal.
```bash
$ chaturbate-dvr -u my_lovely_channel_name
.o88b. db db .d8b. d888888b db db d8888b. d8888b. .d8b. d888888b d88888b
d8P Y8 88 88 d8' `8b `~~88~~' 88 88 88 `8D 88 `8D d8' `8b `~~88~~' 88'
8P 88ooo88 88ooo88 88 88 88 88oobY' 88oooY' 88ooo88 88 88ooooo
8b 88~~~88 88~~~88 88 88 88 88`8b 88~~~b. 88~~~88 88 88~~~~~
Y8b d8 88 88 88 88 88 88b d88 88 `88. 88 8D 88 88 88 88.
`Y88P' YP YP YP YP YP ~Y8888P' 88 YD Y8888P' YP YP YP Y88888P
d8888b. db db d8888b.
88 `8D 88 88 88 `8D
88 88 Y8 8P 88oobY'
88 88 `8b d8' 88`8b
88 .8D `8bd8' 88 `88.
Y8888D' YP 88 YD
---
2020/02/13 18:05:22 my_lovely_channel_name is online! fetching...
2020/02/13 18:05:24 the video will be saved as "2020-02-13_22-16-27.ts".
2020/02/13 18:05:28 fetching media_w402018999_b5128000_t64RlBTOjI5Ljk3_9134.ts (size: 936428)
2020/02/13 19:07:06 failed to fetch the video segments, will try again. (1/2)
2020/02/13 19:07:06 failed to fetch the video segments, will try again. (2/2)
2020/02/13 19:07:11 failed to fetch the video segments after retried, my_lovely_channel_name might went offline.
2020/02/13 19:07:11 my_lovely_channel_name is not online, check again after 3 minute(s)...
```
## Help
```bash
NAME:
chaturbate-dvr - watching a specified chaturbate channel and auto saves the stream as local file
USAGE:
main [global options] command [command options] [arguments...]
COMMANDS:
help, h Shows a list of commands or help for one command
GLOBAL OPTIONS:
--username value, -u value channel username to watching
--interval value, -i value minutes to check if a channel goes online or not (default: 1)
--strip value, -s value MB sizes to split the video into chunks (default: 0)
--resolution 240, -r 240 Video resolution, could be 240, `480`, `540`, `720`, `1080` (default: "1080")
--resolution-fallback up, --rf up Looking for larger or smaller resolution (up for larger, `down` for smaller) if a specified resolution was not found (default: "down")
--fps value, -f value Preferred framerate, only works if streaming source supports it, otherwise it will always be 30 FPS (default: "60")
--help, -h show help (default: false)
--version, -v print the version (default: false)
```

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,9 +0,0 @@
version: "3.0"
services:
chaturbate-dvr:
build: .
environment:
- USERNAME=my_lovely_channel_name
volumes:
- ./video/my_lovely_channel_name:/usr/src/app/video

View File

@ -1,23 +0,0 @@
module github.com/YamiOdymel/chaturbate-dvr
go 1.19
require (
github.com/TwiN/go-color v1.1.0
github.com/grafov/m3u8 v0.11.1
github.com/parnurzeal/gorequest v0.2.16
github.com/urfave/cli/v2 v2.3.0
)
require (
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect
github.com/elazarl/goproxy v0.0.0-20210801061803-8e322dfb79c4 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/russross/blackfriday/v2 v2.0.1 // indirect
github.com/samber/lo v1.38.1 // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/smartystreets/goconvey v1.7.2 // indirect
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect
golang.org/x/net v0.0.0-20211109214657-ef0fda0de508 // indirect
moul.io/http2curl v1.0.0 // indirect
)

View File

@ -1,46 +0,0 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/TwiN/go-color v1.1.0 h1:yhLAHgjp2iAxmNjDiVb6Z073NE65yoaPlcki1Q22yyQ=
github.com/TwiN/go-color v1.1.0/go.mod h1:aKVf4e1mD4ai2FtPifkDPP5iyoCwiK08YGzGwerjKo0=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/elazarl/goproxy v0.0.0-20210801061803-8e322dfb79c4 h1:lS3P5Nw3oPO05Lk2gFiYUOL3QPaH+fRoI1wFOc4G1UY=
github.com/elazarl/goproxy v0.0.0-20210801061803-8e322dfb79c4/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/grafov/m3u8 v0.11.1 h1:igZ7EBIB2IAsPPazKwRKdbhxcoBKO3lO1UY57PZDeNA=
github.com/grafov/m3u8 v0.11.1/go.mod h1:nqzOkfBiZJENr52zTVd/Dcl03yzphIMbJqkXGu+u080=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/parnurzeal/gorequest v0.2.16 h1:T/5x+/4BT+nj+3eSknXmCTnEVGSzFzPGdpqmUVVZXHQ=
github.com/parnurzeal/gorequest v0.2.16/go.mod h1:3Kh2QUMJoqw3icWAecsyzkpY7UzRfDhbRdTjtNwNiUE=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg=
github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM=
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM=
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20211109214657-ef0fda0de508 h1:v3NKo+t/Kc3EASxaKZ82lwK6mCf4ZeObQBduYFZHo7c=
golang.org/x/net v0.0.0-20211109214657-ef0fda0de508/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
moul.io/http2curl v1.0.0 h1:6XwpyZOYsgZJrU8exnG87ncVkU1FVCcTRpwzOkTDUi8=
moul.io/http2curl v1.0.0/go.mod h1:f6cULg+e4Md/oW1cYmwW4IWQOVl2lGbmCNGOHvzX2kE=

View File

@ -1,510 +0,0 @@
package main
import (
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"os"
"regexp"
"strconv"
"strings"
"sync"
"time"
"github.com/TwiN/go-color"
"github.com/samber/lo"
"github.com/grafov/m3u8"
"github.com/parnurzeal/gorequest"
"github.com/urfave/cli/v2"
)
// chaturbateURL is the base url of the website.
const chaturbateURL = "https://chaturbate.com/"
// retriesAfterOnlined tells the retries for stream when disconnected but not really offlined.
var retriesAfterOnlined = 0
// temp stores the used segment to prevent fetched the duplicates.
var temp []string
// segmentIndex is current stored segment index.
var segmentIndex int
// segmentMap is the map stores temporary video segments, it will be merged into master video file then got deleted.
var segmentMap map[string][]byte = make(map[string][]byte)
var segmentMapLock sync.Mutex
// stripLimit reprsents the maximum Bytes sizes to split the video into chunks.
var stripLimit int
// stripQuota represents how many Bytes left til the next video chunk stripping.
var stripQuota int
// preferredFPS represents the preferred framerate.
var preferredFPS string
// preferredResolution represents the preferred resolution, e.g. `240`, `480`, `540`, `720`, `1080`.
var preferredResolution string
// preferredResolutionFallback represents the preferred resolution fallback, `up`, `down` or `no`.
var preferredResolutionFallback string
// path save video
const savePath = "video"
// error/message handler
var (
errInternal = errors.New("err")
errNoUsername = errors.New("recording: channel username required `-u [USERNAME]` option")
errSegRetFail = color.Colorize(color.Red, ("[FAILED] to fetch the video segments after retried, %s might went offline or is in ticket/privat show."))
errSegRetFailOnline = color.Colorize(color.Red, ("[FAILED] to fetch the video segments, will try again. [%d/10]"))
infoIsOnline = color.Colorize(color.Green, ("[RECORDING] %s is online! start fetching.."))
infoBackOnline = color.Colorize(color.Green, ("[INFO] %s is back online!"))
infoMergeSegment = color.Colorize(color.Green, ("[INFO] inserting %d segment to the master file. [total: %d]"))
infoSkipped = color.Colorize(color.Blue, ("[INFO] skipped %s due to the empty body!\n"))
infoNotOnline = color.Colorize(color.Gray, ("[INFO] %s is not online, check again in %d minute(s)"))
warningSegment = color.Colorize(color.Yellow, ("[WARNING] cannot find segment %d, will try again. [%d/5]"))
)
// roomDossier is the struct to parse the HLS source from the content body.
type roomDossier struct {
HLSSource string `json:"hls_source"`
}
// unescapeUnicode escapes the unicode from the content body.
func unescapeUnicode(raw string) string {
str, err := strconv.Unquote(strings.Replace(strconv.Quote(string(raw)), `\\u`, `\u`, -1))
if err != nil {
panic(err)
}
return str
}
// getChannelURL returns the full channel url to the specified user.
func getChannelURL(username string) string {
return fmt.Sprintf("%s%s", chaturbateURL, username)
}
// getBody gets the channel page content body.
func getBody(username string) string {
resp, body, errs := gorequest.New().TLSClientConfig(&tls.Config{InsecureSkipVerify: true}).Get(getChannelURL(username)).End()
if len(errs) > 0 {
log.Println(color.Colorize(color.Red, errs[0].Error()))
}
if resp == nil || resp.StatusCode != 200 {
return ""
}
return body
}
// getOnlineStatus check if the user is currently online by checking the playlist exists in the content body or not.
func getOnlineStatus(username string) bool {
return strings.Contains(getBody(username), "playlist.m3u8")
}
// getHLSSource extracts the playlist url from the room detail page body.
func getHLSSource(body string) (string, string) {
// Get the room data from the page body.
r := regexp.MustCompile(`window\.initialRoomDossier = "(.*?)"`)
matches := r.FindAllStringSubmatch(body, -1)
// Extract the data and get the HLS source URL.
var roomData roomDossier
data := unescapeUnicode(matches[0][1])
err := json.Unmarshal([]byte(data), &roomData)
if err != nil {
panic(err)
}
return roomData.HLSSource, strings.TrimSuffix(roomData.HLSSource, "playlist.m3u8")
}
// parseHLSSource parses the HLS table and return the maximum resolution m3u8 source.
func parseHLSSource(url string, baseURL string) string {
resp, body, errs := gorequest.New().TLSClientConfig(&tls.Config{InsecureSkipVerify: true}).Get(url).End()
if len(errs) > 0 {
log.Println(color.Colorize(color.Red, errs[0].Error()))
}
if resp == nil || resp.StatusCode == 403 {
return ""
}
p, _, _ := m3u8.DecodeFrom(strings.NewReader(body), true)
master, ok := p.(*m3u8.MasterPlaylist)
if !ok {
return ""
}
resolutions := make(map[string][]string)
resolutionInts := []string{}
for _, v := range master.Variants {
resStr := strings.Split(v.Resolution, "x")
resolutionInts = append(resolutionInts, resStr[1])
// If the resolution exists in local, it might be a higher framerate source, store it for later use
if _, ok := resolutions[resStr[1]]; ok {
resolutions[resStr[1]] = append(resolutions[resStr[1]], v.URI)
continue
}
if strings.Contains(v.Name, "FPS:60.0") {
if _, ok := resolutions[resStr[1]]; !ok {
resolutions[resStr[1]] = []string{"", v.URI} // The video has no 30 FPS, we fill it with an empty URI
} else {
resolutions[resStr[1]] = []string{v.URI}
}
} else {
resolutions[resStr[1]] = []string{v.URI}
}
}
log.Printf("Found available resolutions: %s", strings.TrimPrefix(lo.Reduce(resolutionInts, func(prev string, cur string, _ int) string {
return fmt.Sprintf("%s, %s", prev, cur)
}, ""), ", "))
pickedResolution, ok := resolutions[preferredResolution]
if !ok {
var comparison []string
if preferredResolutionFallback == "down" {
comparison = lo.Reverse(lo.Map(resolutionInts, func(v string, _ int) string { return v }))
} else {
comparison = resolutionInts
}
fallbackResolution, ok := lo.Find(comparison, func(v string) bool {
sizeInt, _ := strconv.Atoi(v)
prefInt, _ := strconv.Atoi(preferredResolution)
//
if preferredResolutionFallback == "down" {
return sizeInt < prefInt
} else {
return sizeInt > prefInt
}
})
if ok {
pickedResolution = resolutions[fallbackResolution]
log.Printf("Preferred video resolution %sp not found, use %sp instead.", preferredResolution, fallbackResolution)
} else {
if preferredResolutionFallback == "down" {
pickedResolution = resolutions[resolutionInts[0]]
log.Printf("No fallback video resolution was found, use worse quality %sp instead.", resolutionInts[0])
} else {
pickedResolution = resolutions[resolutionInts[len(resolutionInts)-1]]
log.Printf("No fallback video resolution was found, use best quality %sp instead.", resolutionInts[len(resolutionInts)-1])
}
}
} else {
log.Printf("Fetching video resolution in %sp.", preferredResolution)
}
var uri string
if preferredFPS == "60" && len(pickedResolution) > 1 {
log.Printf("Fetching video in 60 FPS.")
uri = pickedResolution[1]
} else {
log.Printf("Fetching video in 30 FPS.")
uri = pickedResolution[0]
if uri == "" {
log.Printf("The video has no 30 FPS, use 60 FPS instead.")
uri = pickedResolution[1]
}
}1
return fmt.Sprintf("%s%s", baseURL, uri)
}
// parseM3U8Source gets the current segment list, the channel might goes offline if 403 was returned.
func parseM3U8Source(url string) (chunks []*m3u8.MediaSegment, wait float64, err error) {
resp, body, errs := gorequest.New().TLSClientConfig(&tls.Config{InsecureSkipVerify: true}).Get(url).End()
if len(errs) > 0 {
log.Println(color.Colorize(color.Red, errs[0].Error()))
}
// Retry after 3 seconds if the connection lost or status code returns 403 (the channel might went offline).
if len(errs) > 0 || resp == nil || resp.StatusCode == http.StatusForbidden {
return nil, 3, errInternal
}
// Decode the segment table.
p, _, err := m3u8.DecodeFrom(strings.NewReader(body), true)
if err != nil {
log.Println(color.Colorize(color.Red, err.Error()))
}
media, ok := p.(*m3u8.MediaPlaylist)
if !ok {
return nil, 3, errInternal
}
wait = media.TargetDuration / 1.5
// Ignore the empty segments.
for _, v := range media.Segments {
if v != nil {
chunks = append(chunks, v)
}
}
return
}
// capture captures the specified channel streaming.
func capture(username string) {
// Define the video filename by current time //04.09.22 added username into filename mK33y.
filename := username + "_" + time.Now().Format("2006-01-02_15-04-05")
var m3u8Source, baseURL, hlsSource string
var tried int
for {
tried++
//
if tried > 10 {
panic(errors.New("cannot fetch the Playlist correctly after 10 tries"))
}
// Get the channel page content body.
body := getBody(username)
//
if body == "" {
continue
}
// Get the master playlist URL from extracting the channel body.
hlsSource, baseURL = getHLSSource(body)
// Get the best resolution m3u8 by parsing the HLS source table.
m3u8Source = parseHLSSource(hlsSource, baseURL)
//
if m3u8Source != "" {
break
}
<-time.After(time.Millisecond * 500)
}
// Create the master video file.
masterFile, err := os.OpenFile("./"+savePath+"/"+filename+".ts", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0777)
if err != nil {
log.Println(color.Colorize(color.Red, err.Error()))
}
//
log.Printf("the video will be saved as \"./"+savePath+"/%s\".", filename+".ts")
go combineSegment(masterFile, filename)
watchStream(m3u8Source, username, masterFile, filename, baseURL)
}
// watchStream watches the stream and ends if the channel went offline.
func watchStream(m3u8Source string, username string, masterFile *os.File, filename string, baseURL string) {
// Keep fetching the stream chunks until the playlist cannot be accessed after retried x times.
for {
// Get the chunks.
chunks, wait, err := parseM3U8Source(m3u8Source)
// Exit the fetching loop if the channel went offline.
if err != nil {
if retriesAfterOnlined > 10 {
log.Printf(errSegRetFail, username)
break
} else {
log.Printf(errSegRetFailOnline, retriesAfterOnlined)
retriesAfterOnlined++
// Wait to fetch the next playlist.
<-time.After(time.Duration(wait*1000) * time.Millisecond)
continue
}
}
if retriesAfterOnlined != 0 {
log.Printf(infoBackOnline, username)
retriesAfterOnlined = 0
}
for _, v := range chunks {
// Ignore the duplicated chunks.
if isDuplicateSegment(v.URI) {
continue
}
segmentIndex++
go fetchSegment(masterFile, v, baseURL, filename, segmentIndex)
}
<-time.After(time.Duration(wait*1000) * time.Millisecond)
}
}
// isDuplicateSegment returns true if the segment is already been fetched.
func isDuplicateSegment(URI string) bool {
for _, v := range temp {
if URI[len(URI)-10:] == v {
return true
}
}
temp = append(temp, URI[len(URI)-10:])
return false
}
// combineSegment combines the segments to the master video file in the background.
// fixed segment problems mK33y.
// still needs some attention here
func combineSegment(master *os.File, filename string) {
index := 1
stripIndex := 1
var retry int
<-time.After(4 * time.Second)
for {
<-time.After(300 * time.Millisecond)
if index >= segmentIndex {
<-time.After(1 * time.Second)
continue
}
if _, ok := segmentMap[fmt.Sprintf("./%s/%s~%d.ts", savePath, filename, index)]; !ok {
if retry >= 5 {
index++
retry = 0
continue
}
if retry != 0 {
log.Printf(warningSegment, index, retry)
}
retry++
<-time.After(time.Duration(1*retry) * time.Second)
continue
}
if retry != 0 {
retry = 0
}
//
b := segmentMap[fmt.Sprintf("./%s/%s~%d.ts", savePath, filename, index)]
//
var err error
if stripLimit != 0 && stripQuota <= 0 {
newMasterFilename := "./" + savePath + "/" + filename + "_" + strconv.Itoa(stripIndex) + ".ts"
master, err = os.OpenFile(newMasterFilename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0777)
if err != nil {
log.Println(color.Colorize(color.Red, err.Error()))
}
log.Printf("exceeded the specified stripping limit, creating new video file. (file: %s)", newMasterFilename)
stripQuota = stripLimit
stripIndex++
}
master.Write(b)
//
log.Printf(infoMergeSegment, index, segmentIndex)
segmentMapLock.Lock()
delete(segmentMap, fmt.Sprintf("./%s/%s~%d.ts", savePath, filename, index))
segmentMapLock.Unlock()
index++
}
}
// fetchSegment fetches the segment and append to the master file.
func fetchSegment(master *os.File, segment *m3u8.MediaSegment, baseURL string, filename string, index int) {
_, body, _ := gorequest.New().TLSClientConfig(&tls.Config{InsecureSkipVerify: true}).Get(fmt.Sprintf("%s%s", baseURL, segment.URI)).EndBytes()
log.Printf("fetching %s (size: %d)\n", segment.URI, len(body))
if len(body) == 0 {
log.Printf(infoSkipped, segment.URI)
return
}
stripQuota -= len(body)
segmentMapLock.Lock()
segmentMap[fmt.Sprintf("./%s/%s~%d.ts", savePath, filename, index)] = body
segmentMapLock.Unlock()
}
// endpoint implements the application main function endpoint.
func endpoint(c *cli.Context) error {
if c.String("username") == "" {
log.Fatal(errNoUsername)
}
// Converts `strip` from MiB to Bytes
stripLimit = c.Int("strip") * 1024 * 1024
stripQuota = c.Int("strip") * 1024 * 1024
//
preferredFPS = c.String("fps")
preferredResolution = c.String("resolution")
preferredResolutionFallback = c.String("resolution-fallback")
//
fmt.Println(" .o88b. db db .d8b. d888888b db db d8888b. d8888b. .d8b. d888888b d88888b")
fmt.Println("d8P Y8 88 88 d8' `8b `~~88~~' 88 88 88 `8D 88 `8D d8' `8b `~~88~~' 88'")
fmt.Println("8P 88ooo88 88ooo88 88 88 88 88oobY' 88oooY' 88ooo88 88 88ooooo")
fmt.Println("8b 88~~~88 88~~~88 88 88 88 88`8b 88~~~b. 88~~~88 88 88~~~~~")
fmt.Println("Y8b d8 88 88 88 88 88 88b d88 88 `88. 88 8D 88 88 88 88.")
fmt.Println(" `Y88P' YP YP YP YP YP ~Y8888P' 88 YD Y8888P' YP YP YP Y88888P")
fmt.Println("d8888b. db db d8888b.")
fmt.Println("88 `8D 88 88 88 `8D")
fmt.Println("88 88 Y8 8P 88oobY'")
fmt.Println("88 88 `8b d8' 88`8b")
fmt.Println("88 .8D `8bd8' 88 `88.")
fmt.Println("Y8888D' YP 88 YD")
fmt.Println("---")
// Mkdir video folder
if _, err := os.Stat("./" + savePath); os.IsNotExist(err) {
os.Mkdir("./"+savePath, 0777)
}
//
if c.Int("strip") != 0 {
log.Printf("specifying stripping limit as %d MiB(s)", c.Int("strip"))
}
for {
// Capture the stream if the user is currently online.
if getOnlineStatus(c.String("username")) {
log.Printf(infoIsOnline, c.String("username"))
capture(c.String("username"))
segmentIndex = 0
temp = []string{}
retriesAfterOnlined = 0
continue
}
// Otherwise we keep checking the channel status until the user is online.
log.Printf(infoNotOnline, c.String("username"), c.Int("interval"))
<-time.After(time.Minute * time.Duration(c.Int("interval")))
}
}
func main() {
app := &cli.App{
Version: "0.94 Alpha",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "username",
Aliases: []string{"u"},
Value: "",
Usage: "channel username to watching",
},
&cli.IntFlag{
Name: "interval",
Aliases: []string{"i"},
Value: 1,
Usage: "minutes to check if a channel goes online or not",
},
&cli.IntFlag{
Name: "strip",
Aliases: []string{"s"},
Value: 0,
Usage: "MB sizes to split the video into chunks",
},
&cli.StringFlag{
Name: "resolution",
Aliases: []string{"r"},
Value: "1080",
Usage: "Video resolution, could be `240`, `480`, `540`, `720`, `1080`",
},
&cli.StringFlag{
Name: "resolution-fallback",
Aliases: []string{"rf"},
Value: "down",
Usage: "Looking for larger or smaller resolution (`up` for larger, `down` for smaller) if a specified resolution was not found",
},
&cli.StringFlag{
Name: "fps",
Aliases: []string{"f"},
Value: "60",
Usage: "Preferred framerate, only works if streaming source supports it, otherwise it will always be 30 FPS",
},
},
Name: "chaturbate-dvr",
Usage: "watching a specified chaturbate channel and auto saves the stream as local file",
Action: endpoint,
}
err := app.Run(os.Args)
if err != nil {
log.Fatal(err)
}
}

View File

@ -8,4 +8,4 @@ RUN go mod download && go mod verify
COPY . .
RUN go build
CMD [ "sh", "-c", "./chaturbate-dvr -u $USERNAME -ui no start" ]
CMD [ "sh", "-c", "./chaturbate-dvr -u $USERNAME start" ]

106
README.md
View File

@ -1,13 +1,54 @@
# Chaturbate DVR
The program records Chaturbate stream, supports 32-bit/64-bit Windows, macOS, Linux (or ARM), comes with a Web UI.
The program can records **multiple** Chaturbate streams, supports 32-bit/64-bit/ARM macOS, Windows, Linux, can be run on Docker.
For Chaturbate-**only**, private/ticket stream is **unsupported**.
**[DMCA WARNING](https://www.dmca.com/)**: Contents on Chaturbate are copyrighted, you should not copy, share, distribute the content.
## Hello
&nbsp;
![image](./assets/image_1.png)
![image](./assets/image_2.png)
## Usage
1. Download **`source code (zip)`** from **[Release](https://github.com/teacat/chaturbate-dvr/releases)** page.
2. Unzip **`/bin`** folder and look up for executable that **fits your system**.
&nbsp;
**🌐 Start the program with the Web UI**
Visit [`http://localhost:8080`](http://localhost:8080) to use the Web UI.
```yaml
# Windows (or double-click `chaturbate-dvr.exe` to open)
$ chaturbate-dvr.exe
# macOS or Linux
$ chaturbate-dvr
```
&nbsp;
**💻 or... Run as a command-line tool**
Run the program with a channel name (`-u CHANNEL_USERNAME`) records the channel immediately, and the Web UI will be disabled.
```yaml
# Windows
$ chaturbate-dvr.exe -u CHANNEL_USERNAME
# macOS or Linux
$ chaturbate-dvr -u CHANNEL_USERNAME
```
&nbsp;
## Preview
![image_1](https://github.com/teacat/chaturbate-dvr/assets/7308718/c6d17ffe-eba7-4296-9315-f501489d85f3)
![image_2](https://github.com/teacat/chaturbate-dvr/assets/7308718/d02923e0-574d-4a15-a373-8b0599101e3f)
**or... Command-line tool**
```
$ ./chaturbate-dvr -u emillybrowm start
@ -36,30 +77,6 @@ $ ./chaturbate-dvr -u emillybrowm start
&nbsp;
## Usage
Start the program also enables the Web UI. Visit [`http://localhost:8080`](http://localhost:8080) to use the Web UI to manage channels.
```yaml
# Windows
$ chaturbate-dvr.exe start
# macOS or Linux
$ chaturbate-dvr start
```
&nbsp;
**💻 or... Run as a command-line tool**
Run the program with a channel name (`-u CHANNEL_USERNAME`) records the channel immediately, and the Web UI will be disabled.
```yaml
$ chaturbate-dvr -u CHANNEL_USERNAME start
```
&nbsp;
## Help
```bash
@ -75,7 +92,6 @@ VERSION:
1.0.0
COMMANDS:
start
help, h Shows a list of commands or help for one command
GLOBAL OPTIONS:
@ -96,25 +112,25 @@ GLOBAL OPTIONS:
```yaml
# Records in 720p/60fps
$ chaturbate-dvr -u yamiodymel -f 60 -r 720 start
$ chaturbate-dvr -u yamiodymel -r 720 -f 60
# Split the video every 30 minutes
$ chaturbate-dvr -u yamiodymel -sd 30 start
$ chaturbate-dvr -u yamiodymel -sd 30
# Split the video every 1024 MB
$ chaturbate-dvr -u yamiodymel -sf 1024 start
$ chaturbate-dvr -u yamiodymel -sf 1024
# Change output filename pattern
$ chaturbate-dvr -u yamiodymel -fp video/{{.Username}}/{{.Year}}-{{.Month}}-{{.Day}}_{{.Hour}}-{{.Minute}}-{{.Second}}_{{.Sequence}} start
$ chaturbate-dvr -u yamiodymel -fp video/{{.Username}}/{{.Year}}-{{.Month}}-{{.Day}}_{{.Hour}}-{{.Minute}}-{{.Second}}_{{.Sequence}}
```
If the `-u CHANNEL_NAME` flag was not specified, the settings will be default settings for Web UI to create channels.
When runs in Web UI mode, the settings will be default settings for Web UI to create channels.
&nbsp;
## 📺 Framerate & Resolution / Fallback
Fallback indicates what to do when there's no expected target resolution, imagine the situation:
Fallback indicates what to do when there's no expected target resolution, situation:
```
Availables: 1080p, 720p, 240p
@ -160,3 +176,23 @@ Pattern: video/{{.Username}}/{{.Year}}-{{.Month}}-{{.Day}}_{{.Hour}}-{{.Minute}}
```
※ The file will be saved as `.ts` format and it's not configurable.
&nbsp;
## 💬 Verbose Log
Change `-log-level` to `DEBUG` to see more details in terminal, like Duration and Size.
```yaml
# Availables: DEBUG, INFO, WARN, ERROR
$ chaturbate-dvr -u hepbugbear -log-level DEBUG
[2024-01-24 01:18:11] [INFO] [hepbugbear] segment #0 written
[2024-01-24 01:18:11] [DEBUG] [hepbugbear] duration: 00:00:06, size: 0.00 MiB
[2024-01-24 01:18:11] [INFO] [hepbugbear] segment #1 written
[2024-01-24 01:18:11] [DEBUG] [hepbugbear] duration: 00:00:06, size: 1.36 MiB
[2024-01-24 01:18:11] [INFO] [hepbugbear] segment #2 written
[2024-01-24 01:18:11] [DEBUG] [hepbugbear] duration: 00:00:06, size: 2.72 MiB
[2024-01-24 01:18:12] [DEBUG] [hepbugbear] segment #3 fetched
[2024-01-24 01:18:13] [INFO] [hepbugbear] segment #3 written
[2024-01-24 01:18:13] [DEBUG] [hepbugbear] duration: 00:00:10, size: 4.08 MiB
```

26
README_DEV.md Normal file
View File

@ -0,0 +1,26 @@
Compile for 64-bit Windows, macOS, Linux:
```
GOOS=windows GOARCH=amd64 go build -o bin/windows/chatubrate-dvr.exe &&
GOOS=darwin GOARCH=amd64 go build -o bin/darwin/chatubrate-dvr &&
GOOS=linux GOARCH=amd64 go build -o bin/linux/chatubrate-dvr
```
Compile for arm64 Windows, macOS, Linux:
```
GOOS=windows GOARCH=arm64 go build -o bin/arm64/windows/chatubrate-dvr.exe &&
GOOS=darwin GOARCH=arm64 go build -o bin/arm64/darwin/chatubrate-dvr &&
GOOS=linux GOARCH=arm64 go build -o bin/arm64/linux/chatubrate-dvr
```
or Compile All at once:
```
GOOS=windows GOARCH=amd64 go build -o bin/windows/chatubrate-dvr.exe &&
GOOS=darwin GOARCH=amd64 go build -o bin/darwin/chatubrate-dvr &&
GOOS=linux GOARCH=amd64 go build -o bin/linux/chatubrate-dvr &&
GOOS=windows GOARCH=arm64 go build -o bin/arm64/windows/chatubrate-dvr.exe &&
GOOS=darwin GOARCH=arm64 go build -o bin/arm64/darwin/chatubrate-dvr &&
GOOS=linux GOARCH=arm64 go build -o bin/arm64/linux/chatubrate-dvr
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
bin/darwin/chatubrate-dvr Normal file

Binary file not shown.

BIN
bin/linux/chatubrate-dvr Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -4,6 +4,6 @@ services:
chaturbate-dvr:
build: .
environment:
- USERNAME=my_lovely_channel_name
- USERNAME=CHANNEL_USERNAME
volumes:
- ./video/my_lovely_channel_name:/usr/src/app/video
- ./videos:/usr/src/app/videos

4
go.mod
View File

@ -16,7 +16,6 @@ require (
github.com/chenzhuoyu/iasm v0.9.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/cors v1.5.0 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
@ -24,11 +23,13 @@ require (
github.com/goccy/go-json v0.10.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/kr/pretty v0.3.0 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
github.com/rogpeppe/go-internal v1.8.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
@ -40,5 +41,6 @@ require (
golang.org/x/sys v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

45
go.sum
View File

@ -1,11 +1,8 @@
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
github.com/bytedance/sonic v1.10.1 h1:7a1wuFXL1cMy7a3f7/VFcEtriuXQnUBhtoVfOZiaysc=
github.com/bytedance/sonic v1.10.1/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
@ -13,13 +10,12 @@ github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/gin-contrib/cors v1.5.0 h1:DgGKV7DDoOn36DFkNtbHrjoRiT5ExCe+PC9/xp7aKvk=
github.com/gin-contrib/cors v1.5.0/go.mod h1:TvU7MAZ3EwrPLI2ztzTt3tqgvBCq+wn8WpZmfADjupI=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
@ -30,8 +26,6 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-playground/validator/v10 v10.15.5 h1:LEBecTWb/1j5TNY1YYG2RcOUN3R7NLylN+x8TTueE24=
github.com/go-playground/validator/v10 v10.15.5/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
@ -47,11 +41,17 @@ github.com/grafov/m3u8 v0.12.0/go.mod h1:nqzOkfBiZJENr52zTVd/Dcl03yzphIMbJqkXGu+
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
@ -61,12 +61,14 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA=
@ -80,8 +82,7 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
@ -92,41 +93,31 @@ github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6S
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.5.0 h1:jpGode6huXQxcskEIpOCvrU+tzo81b6+oFLUYXWtH/Y=
golang.org/x/arch v0.5.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM=
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.16.0 h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos=
golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
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/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -93,12 +93,6 @@ func main() {
//},
},
Action: start,
Commands: []*cli.Command{
{
Name: "start",
Action: start,
},
},
}
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
@ -156,5 +150,7 @@ func startWeb(c *cli.Context) error {
r.POST("/api/get_settings", handler.NewGetSettingsHandler(c).Handle)
r.POST("/api/terminate_program", handler.NewTerminateProgramHandler(c).Handle)
fmt.Printf("👋 Visit http://localhost:%s to use the Web UI\n", c.String("port"))
return r.Run(fmt.Sprintf(":%s", c.String("port")))
}