mirror of
https://github.com/teacat/chaturbate-dvr.git
synced 2025-10-29 16:59:59 +00:00
Merge branch 'master' of github.com:teacat/chaturbate-dvr
This commit is contained in:
commit
ed1bcf90ec
16
go.mod
16
go.mod
@ -1,16 +1,20 @@
|
|||||||
module github.com/YamiOdymel/chaturbate-dvr
|
module github.com/YamiOdymel/chaturbate-dvr
|
||||||
|
|
||||||
go 1.12
|
go 1.19
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/elazarl/goproxy v0.0.0-20210801061803-8e322dfb79c4 // indirect
|
github.com/TwiN/go-color v1.1.0
|
||||||
github.com/grafov/m3u8 v0.11.1
|
github.com/grafov/m3u8 v0.11.1
|
||||||
github.com/parnurzeal/gorequest v0.2.16
|
github.com/parnurzeal/gorequest v0.2.16
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
|
||||||
github.com/smartystreets/goconvey v1.7.2 // indirect
|
|
||||||
github.com/stretchr/testify v1.7.0 // indirect
|
|
||||||
github.com/teacat/pathx v0.0.0-20201109184104-55ec346a0c6d
|
github.com/teacat/pathx v0.0.0-20201109184104-55ec346a0c6d
|
||||||
github.com/urfave/cli/v2 v2.3.0
|
github.com/urfave/cli/v2 v2.3.0
|
||||||
|
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/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
||||||
|
github.com/smartystreets/goconvey v1.7.2 // indirect
|
||||||
|
github.com/stretchr/testify v1.7.0 // indirect
|
||||||
golang.org/x/net v0.0.0-20211109214657-ef0fda0de508 // indirect
|
golang.org/x/net v0.0.0-20211109214657-ef0fda0de508 // indirect
|
||||||
moul.io/http2curl v1.0.0 // indirect
|
moul.io/http2curl v1.0.0 // indirect
|
||||||
)
|
)
|
||||||
9
go.sum
9
go.sum
@ -1,11 +1,12 @@
|
|||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
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 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/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/elazarl/goproxy v0.0.0-20210801061803-8e322dfb79c4 h1:lS3P5Nw3oPO05Lk2gFiYUOL3QPaH+fRoI1wFOc4G1UY=
|
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 v0.0.0-20210801061803-8e322dfb79c4/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
|
||||||
github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2 h1:dWB6v3RcOy03t/bUadywsbyrQwCqZeNIEX6M1OtSZOM=
|
|
||||||
github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8=
|
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 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
@ -40,14 +41,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
|
|||||||
golang.org/x/net v0.0.0-20211109214657-ef0fda0de508 h1:v3NKo+t/Kc3EASxaKZ82lwK6mCf4ZeObQBduYFZHo7c=
|
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/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/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
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 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||||
|
|||||||
716
main.go
716
main.go
@ -1,352 +1,364 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/teacat/pathx"
|
"github.com/TwiN/go-color"
|
||||||
|
"github.com/teacat/pathx"
|
||||||
"github.com/grafov/m3u8"
|
|
||||||
"github.com/parnurzeal/gorequest"
|
"github.com/grafov/m3u8"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/parnurzeal/gorequest"
|
||||||
)
|
"github.com/urfave/cli/v2"
|
||||||
|
)
|
||||||
// chaturbateURL is the base url of the website.
|
|
||||||
const chaturbateURL = "https://chaturbate.com/"
|
// 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
|
// retriesAfterOnlined tells the retries for stream when disconnected but not really offlined.
|
||||||
|
var retriesAfterOnlined = 0
|
||||||
// bucket stores the used segment to prevent fetched the duplicates.
|
|
||||||
var bucket []string
|
// temp stores the used segment to prevent fetched the duplicates.
|
||||||
|
var temp []string
|
||||||
// segmentIndex is current stored segment index.
|
|
||||||
var segmentIndex int
|
// segmentIndex is current stored segment index.
|
||||||
|
var segmentIndex int
|
||||||
// stripLimit reprsents the maximum Bytes sizes to split the video into chunks.
|
|
||||||
var stripLimit int
|
// 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
|
// stripQuota represents how many Bytes left til the next video chunk stripping.
|
||||||
|
var stripQuota int
|
||||||
// path save video
|
|
||||||
const savePath = "video"
|
// path save video
|
||||||
|
const savePath = "video"
|
||||||
var (
|
|
||||||
errInternal = errors.New("err")
|
// error/message handler
|
||||||
errNoUsername = errors.New("chaturbate-dvr: channel username required with `-u [username]` argument")
|
var (
|
||||||
)
|
errInternal = errors.New("err")
|
||||||
|
errNoUsername = errors.New("recording: channel username required `-u [USERNAME]` option")
|
||||||
// roomDossier is the struct to parse the HLS source from the content body.
|
errSegRetFail = color.Colorize(color.Red, ("[FAILED] to fetch the video segments after retried, %s might went offline or is in ticket/privat show."))
|
||||||
type roomDossier struct {
|
errSegRetFailOnline = color.Colorize(color.Red, ("[FAILED] to fetch the video segments, will try again. [%d/10]"))
|
||||||
HLSSource string `json:"hls_source"`
|
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]"))
|
||||||
// unescapeUnicode escapes the unicode from the content body.
|
infoSkipped = color.Colorize(color.Blue, ("[INFO] skipped %s due to the empty body!\n"))
|
||||||
func unescapeUnicode(raw string) string {
|
infoNotOnline = color.Colorize(color.Gray, ("[INFO] %s is not online, check again in %d minute(s)"))
|
||||||
str, err := strconv.Unquote(strings.Replace(strconv.Quote(string(raw)), `\\u`, `\u`, -1))
|
warningSegment = color.Colorize(color.Yellow, ("[WARNING] cannot find segment %d, will try again. [%d/5]"))
|
||||||
if err != nil {
|
)
|
||||||
panic(err)
|
|
||||||
}
|
// roomDossier is the struct to parse the HLS source from the content body.
|
||||||
return str
|
type roomDossier struct {
|
||||||
}
|
HLSSource string `json:"hls_source"`
|
||||||
|
}
|
||||||
// getChannelURL returns the full channel url to the specified user.
|
|
||||||
func getChannelURL(username string) string {
|
// unescapeUnicode escapes the unicode from the content body.
|
||||||
return fmt.Sprintf("%s%s", chaturbateURL, username)
|
func unescapeUnicode(raw string) string {
|
||||||
}
|
str, err := strconv.Unquote(strings.Replace(strconv.Quote(string(raw)), `\\u`, `\u`, -1))
|
||||||
|
if err != nil {
|
||||||
// getBody gets the channel page content body.
|
panic(err)
|
||||||
func getBody(username string) string {
|
}
|
||||||
_, body, _ := gorequest.New().Get(getChannelURL(username)).End()
|
return str
|
||||||
return body
|
}
|
||||||
}
|
|
||||||
|
// getChannelURL returns the full channel url to the specified user.
|
||||||
// getOnlineStatus check if the user is currently online by checking the playlist exists in the content body or not.
|
func getChannelURL(username string) string {
|
||||||
func getOnlineStatus(username string) bool {
|
return fmt.Sprintf("%s%s", chaturbateURL, username)
|
||||||
return strings.Contains(getBody(username), "playlist.m3u8")
|
}
|
||||||
}
|
|
||||||
|
// getBody gets the channel page content body.
|
||||||
// getHLSSource extracts the playlist url from the room detail page body.
|
func getBody(username string) string {
|
||||||
func getHLSSource(body string) (string, string) {
|
_, body, _ := gorequest.New().Get(getChannelURL(username)).End()
|
||||||
// Get the room data from the page body.
|
return body
|
||||||
r := regexp.MustCompile(`window\.initialRoomDossier = "(.*?)"`)
|
}
|
||||||
matches := r.FindAllStringSubmatch(body, -1)
|
|
||||||
|
// getOnlineStatus check if the user is currently online by checking the playlist exists in the content body or not.
|
||||||
// Extract the data and get the HLS source URL.
|
func getOnlineStatus(username string) bool {
|
||||||
var roomData roomDossier
|
return strings.Contains(getBody(username), "playlist.m3u8")
|
||||||
data := unescapeUnicode(matches[0][1])
|
}
|
||||||
err := json.Unmarshal([]byte(data), &roomData)
|
|
||||||
if err != nil {
|
// getHLSSource extracts the playlist url from the room detail page body.
|
||||||
panic(err)
|
func getHLSSource(body string) (string, string) {
|
||||||
}
|
// Get the room data from the page body.
|
||||||
|
r := regexp.MustCompile(`window\.initialRoomDossier = "(.*?)"`)
|
||||||
return roomData.HLSSource, strings.TrimRight(roomData.HLSSource, "playlist.m3u8")
|
matches := r.FindAllStringSubmatch(body, -1)
|
||||||
}
|
|
||||||
|
// Extract the data and get the HLS source URL.
|
||||||
// parseHLSSource parses the HLS table and return the maximum resolution m3u8 source.
|
var roomData roomDossier
|
||||||
func parseHLSSource(url string, baseURL string) string {
|
data := unescapeUnicode(matches[0][1])
|
||||||
_, body, _ := gorequest.New().Get(url).End()
|
err := json.Unmarshal([]byte(data), &roomData)
|
||||||
|
if err != nil {
|
||||||
<-time.After(time.Millisecond * 300)
|
panic(err)
|
||||||
|
}
|
||||||
// Decode the HLS table.
|
|
||||||
p, _, _ := m3u8.DecodeFrom(strings.NewReader(body), true)
|
return roomData.HLSSource, strings.TrimRight(roomData.HLSSource, "playlist.m3u8")
|
||||||
master := p.(*m3u8.MasterPlaylist)
|
}
|
||||||
return fmt.Sprintf("%s%s", baseURL, master.Variants[len(master.Variants)-1].URI)
|
|
||||||
}
|
// parseHLSSource parses the HLS table and return the maximum resolution m3u8 source.
|
||||||
|
func parseHLSSource(url string, baseURL string) string {
|
||||||
// parseM3U8Source gets the current segment list, the channel might goes offline if 403 was returned.
|
_, body, _ := gorequest.New().Get(url).End()
|
||||||
func parseM3U8Source(url string) (chunks []*m3u8.MediaSegment, wait float64, err error) {
|
|
||||||
resp, body, errs := gorequest.New().Get(url).End()
|
<-time.After(time.Millisecond * 300)
|
||||||
// Retry after 3 seconds if the connection lost or status code returns 403 (the channel might went offline).
|
|
||||||
if len(errs) > 0 || resp.StatusCode == http.StatusForbidden {
|
// Decode the HLS table.
|
||||||
return nil, 3, errInternal
|
p, _, _ := m3u8.DecodeFrom(strings.NewReader(body), true)
|
||||||
}
|
master := p.(*m3u8.MasterPlaylist)
|
||||||
|
return fmt.Sprintf("%s%s", baseURL, master.Variants[len(master.Variants)-1].URI)
|
||||||
// Decode the segment table.
|
}
|
||||||
p, _, _ := m3u8.DecodeFrom(strings.NewReader(body), true)
|
|
||||||
media, ok := p.(*m3u8.MediaPlaylist)
|
// parseM3U8Source gets the current segment list, the channel might goes offline if 403 was returned.
|
||||||
if !ok {
|
func parseM3U8Source(url string) (chunks []*m3u8.MediaSegment, wait float64, err error) {
|
||||||
return nil, 3, errInternal
|
resp, body, errs := gorequest.New().Get(url).End()
|
||||||
}
|
// Retry after 3 seconds if the connection lost or status code returns 403 (the channel might went offline).
|
||||||
wait = media.TargetDuration / 1.5
|
if len(errs) > 0 || resp.StatusCode == http.StatusForbidden {
|
||||||
|
return nil, 3, errInternal
|
||||||
// Ignore the empty segments.
|
}
|
||||||
for _, v := range media.Segments {
|
|
||||||
if v != nil {
|
// Decode the segment table.
|
||||||
chunks = append(chunks, v)
|
p, _, _ := m3u8.DecodeFrom(strings.NewReader(body), true)
|
||||||
}
|
media, ok := p.(*m3u8.MediaPlaylist)
|
||||||
}
|
if !ok {
|
||||||
return
|
return nil, 3, errInternal
|
||||||
}
|
}
|
||||||
|
wait = media.TargetDuration / 1.5
|
||||||
// capture captures the specified channel streaming.
|
|
||||||
func capture(username string) {
|
// Ignore the empty segments.
|
||||||
// Define the video filename by current time //04.09.22 added username into filename mK33y.
|
for _, v := range media.Segments {
|
||||||
filename := username + "_" + time.Now().Format("2006-01-02_15-04-05")
|
if v != nil {
|
||||||
// Get the channel page content body.
|
chunks = append(chunks, v)
|
||||||
body := getBody(username)
|
}
|
||||||
// Get the master playlist URL from extracting the channel body.
|
}
|
||||||
hlsSource, baseURL := getHLSSource(body)
|
return
|
||||||
// Get the best resolution m3u8 by parsing the HLS source table.
|
}
|
||||||
m3u8Source := parseHLSSource(hlsSource, baseURL)
|
|
||||||
// Create the master video file.
|
// capture captures the specified channel streaming.
|
||||||
masterFile, _ := os.OpenFile("./"+savePath+"/"+filename+".ts", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0777)
|
func capture(username string) {
|
||||||
//
|
// Define the video filename by current time //04.09.22 added username into filename mK33y.
|
||||||
log.Printf("the video will be saved as \"./"+savePath+"/%s\".", filename+".ts")
|
filename := username + "_" + time.Now().Format("2006-01-02_15-04-05")
|
||||||
|
// Get the channel page content body.
|
||||||
go combineSegment(masterFile, filename)
|
body := getBody(username)
|
||||||
watchStream(m3u8Source, username, masterFile, filename, baseURL)
|
// 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.
|
||||||
// watchStream watches the stream and ends if the channel went offline.
|
m3u8Source := parseHLSSource(hlsSource, baseURL)
|
||||||
func watchStream(m3u8Source string, username string, masterFile *os.File, filename string, baseURL string) {
|
// Create the master video file.
|
||||||
// Keep fetching the stream chunks until the playlist cannot be accessed after retried x times.
|
masterFile, _ := os.OpenFile("./"+savePath+"/"+filename+".ts", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0777)
|
||||||
for {
|
//
|
||||||
// Get the chunks.
|
log.Printf("the video will be saved as \"./"+savePath+"/%s\".", filename+".ts")
|
||||||
chunks, wait, err := parseM3U8Source(m3u8Source)
|
|
||||||
// Exit the fetching loop if the channel went offline.
|
go combineSegment(masterFile, filename)
|
||||||
if err != nil {
|
watchStream(m3u8Source, username, masterFile, filename, baseURL)
|
||||||
if retriesAfterOnlined > 10 {
|
}
|
||||||
log.Printf("failed to fetch the video segments after retried, %s might went offline.", username)
|
|
||||||
break
|
// watchStream watches the stream and ends if the channel went offline.
|
||||||
} else {
|
func watchStream(m3u8Source string, username string, masterFile *os.File, filename string, baseURL string) {
|
||||||
log.Printf("failed to fetch the video segments, will try again. (%d/10)", retriesAfterOnlined)
|
// Keep fetching the stream chunks until the playlist cannot be accessed after retried x times.
|
||||||
retriesAfterOnlined++
|
for {
|
||||||
// Wait to fetch the next playlist.
|
// Get the chunks.
|
||||||
<-time.After(time.Duration(wait*1000) * time.Millisecond)
|
chunks, wait, err := parseM3U8Source(m3u8Source)
|
||||||
continue
|
// Exit the fetching loop if the channel went offline.
|
||||||
}
|
if err != nil {
|
||||||
}
|
if retriesAfterOnlined > 10 {
|
||||||
if retriesAfterOnlined != 0 {
|
log.Printf(errSegRetFail, username)
|
||||||
log.Printf("%s is back online!", username)
|
break
|
||||||
retriesAfterOnlined = 0
|
} else {
|
||||||
}
|
log.Printf(errSegRetFailOnline, retriesAfterOnlined)
|
||||||
for _, v := range chunks {
|
retriesAfterOnlined++
|
||||||
// Ignore the duplicated chunks.
|
// Wait to fetch the next playlist.
|
||||||
if isDuplicateSegment(v.URI) {
|
<-time.After(time.Duration(wait*1000) * time.Millisecond)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
segmentIndex++
|
}
|
||||||
go fetchSegment(masterFile, v, baseURL, filename, segmentIndex)
|
if retriesAfterOnlined != 0 {
|
||||||
}
|
log.Printf(infoBackOnline, username)
|
||||||
<-time.After(time.Duration(wait*1000) * time.Millisecond)
|
retriesAfterOnlined = 0
|
||||||
}
|
}
|
||||||
}
|
for _, v := range chunks {
|
||||||
|
// Ignore the duplicated chunks.
|
||||||
// isDuplicateSegment returns true if the segment is already been fetched.
|
if isDuplicateSegment(v.URI) {
|
||||||
func isDuplicateSegment(URI string) bool {
|
continue
|
||||||
for _, v := range bucket {
|
}
|
||||||
if URI[len(URI)-10:] == v {
|
segmentIndex++
|
||||||
return true
|
go fetchSegment(masterFile, v, baseURL, filename, segmentIndex)
|
||||||
}
|
}
|
||||||
}
|
<-time.After(time.Duration(wait*1000) * time.Millisecond)
|
||||||
bucket = append(bucket, URI[len(URI)-10:])
|
}
|
||||||
return false
|
}
|
||||||
}
|
|
||||||
|
// isDuplicateSegment returns true if the segment is already been fetched.
|
||||||
// combineSegment combines the segments to the master video file in the background.
|
func isDuplicateSegment(URI string) bool {
|
||||||
// fixed segment problems mK33y.
|
for _, v := range temp {
|
||||||
// still needs some attention here
|
if URI[len(URI)-10:] == v {
|
||||||
func combineSegment(master *os.File, filename string) {
|
return true
|
||||||
index := 1
|
}
|
||||||
delete := 1
|
}
|
||||||
stripIndex := 1
|
temp = append(temp, URI[len(URI)-10:])
|
||||||
var retry int
|
return false
|
||||||
<-time.After(4 * time.Second)
|
}
|
||||||
|
|
||||||
for {
|
// combineSegment combines the segments to the master video file in the background.
|
||||||
<-time.After(300 * time.Millisecond)
|
// fixed segment problems mK33y.
|
||||||
|
// still needs some attention here
|
||||||
if index >= segmentIndex {
|
func combineSegment(master *os.File, filename string) {
|
||||||
<-time.After(1 * time.Second)
|
index := 1
|
||||||
continue
|
delete := 1
|
||||||
}
|
stripIndex := 1
|
||||||
|
var retry int
|
||||||
if !pathx.Exists(fmt.Sprintf("./%s/%s~%d.ts", savePath, filename, index)) {
|
<-time.After(4 * time.Second)
|
||||||
if retry >= 5 {
|
|
||||||
index++
|
for {
|
||||||
retry = 0
|
<-time.After(300 * time.Millisecond)
|
||||||
continue
|
|
||||||
}
|
if index >= segmentIndex {
|
||||||
if retry != 0 {
|
<-time.After(1 * time.Second)
|
||||||
log.Printf("cannot find segment %d, will try again. (%d/5)", index, retry)
|
continue
|
||||||
}
|
}
|
||||||
retry++
|
|
||||||
<-time.After(time.Duration(1*retry) * time.Second)
|
if !pathx.Exists(fmt.Sprintf("./%s/%s~%d.ts", savePath, filename, index)) {
|
||||||
continue
|
if retry >= 5 {
|
||||||
}
|
index++
|
||||||
if retry != 0 {
|
retry = 0
|
||||||
retry = 0
|
continue
|
||||||
}
|
}
|
||||||
//
|
if retry != 0 {
|
||||||
b, _ := ioutil.ReadFile(fmt.Sprintf("./%s/%s~%d.ts", savePath, filename, index))
|
log.Printf(warningSegment, index, retry)
|
||||||
//
|
}
|
||||||
if stripLimit != 0 && stripQuota <= 0 {
|
retry++
|
||||||
newMasterFilename := "./" + savePath + "/" + filename + "_" + strconv.Itoa(stripIndex) + ".ts"
|
<-time.After(time.Duration(1*retry) * time.Second)
|
||||||
master, _ = os.OpenFile(newMasterFilename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0777)
|
continue
|
||||||
log.Printf("exceeded the specified stripping limit, creating new video file. (file: %s)", newMasterFilename)
|
}
|
||||||
stripQuota = stripLimit
|
if retry != 0 {
|
||||||
stripIndex++
|
retry = 0
|
||||||
}
|
}
|
||||||
master.Write(b)
|
//
|
||||||
log.Printf("inserting %d segment to the master file. (total: %d)", index, segmentIndex)
|
b, _ := ioutil.ReadFile(fmt.Sprintf("./%s/%s~%d.ts", savePath, filename, index))
|
||||||
//
|
//
|
||||||
e := os.Remove(fmt.Sprintf("./%s/%s~%d.ts", savePath, filename, delete))
|
if stripLimit != 0 && stripQuota <= 0 {
|
||||||
if e != nil {
|
newMasterFilename := "./" + savePath + "/" + filename + "_" + strconv.Itoa(stripIndex) + ".ts"
|
||||||
delete--
|
master, _ = os.OpenFile(newMasterFilename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0777)
|
||||||
}
|
log.Printf("exceeded the specified stripping limit, creating new video file. (file: %s)", newMasterFilename)
|
||||||
delete++
|
stripQuota = stripLimit
|
||||||
index++
|
stripIndex++
|
||||||
}
|
}
|
||||||
}
|
master.Write(b)
|
||||||
|
//
|
||||||
// fetchSegment fetches the segment and append to the master file.
|
log.Printf(infoMergeSegment, index, segmentIndex)
|
||||||
func fetchSegment(master *os.File, segment *m3u8.MediaSegment, baseURL string, filename string, index int) {
|
|
||||||
_, body, _ := gorequest.New().Get(fmt.Sprintf("%s%s", baseURL, segment.URI)).EndBytes()
|
e := os.Remove(fmt.Sprintf("./%s/%s~%d.ts", savePath, filename, delete))
|
||||||
log.Printf("fetching %s (size: %d)\n", segment.URI, len(body))
|
//
|
||||||
if len(body) == 0 {
|
if e != nil {
|
||||||
log.Printf("skipped %s due to the empty body!\n", segment.URI)
|
delete--
|
||||||
return
|
}
|
||||||
}
|
delete++
|
||||||
stripQuota -= len(body)
|
index++
|
||||||
//
|
}
|
||||||
f, err := os.OpenFile(fmt.Sprintf("./%s/%s~%d.ts", savePath, filename, index), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0777)
|
}
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
// fetchSegment fetches the segment and append to the master file.
|
||||||
}
|
func fetchSegment(master *os.File, segment *m3u8.MediaSegment, baseURL string, filename string, index int) {
|
||||||
if _, err := f.Write(body); err != nil {
|
_, body, _ := gorequest.New().Get(fmt.Sprintf("%s%s", baseURL, segment.URI)).EndBytes()
|
||||||
panic(err)
|
log.Printf("fetching %s (size: %d)\n", segment.URI, len(body))
|
||||||
}
|
if len(body) == 0 {
|
||||||
}
|
log.Printf(infoSkipped, segment.URI)
|
||||||
|
return
|
||||||
// endpoint implements the application main function endpoint.
|
}
|
||||||
func endpoint(c *cli.Context) error {
|
stripQuota -= len(body)
|
||||||
if c.String("username") == "" {
|
//
|
||||||
log.Fatal(errNoUsername)
|
f, err := os.OpenFile(fmt.Sprintf("./%s/%s~%d.ts", savePath, filename, index), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0777)
|
||||||
}
|
if err != nil {
|
||||||
// Converts `strip` from MiB to Bytes
|
panic(err)
|
||||||
stripLimit = c.Int("strip") * 1024 * 1024
|
}
|
||||||
stripQuota = c.Int("strip") * 1024 * 1024
|
if _, err := f.Write(body); err != nil {
|
||||||
//
|
panic(err)
|
||||||
|
}
|
||||||
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")
|
// endpoint implements the application main function endpoint.
|
||||||
fmt.Println("8b 88~~~88 88~~~88 88 88 88 88`8b 88~~~b. 88~~~88 88 88~~~~~")
|
func endpoint(c *cli.Context) error {
|
||||||
fmt.Println("Y8b d8 88 88 88 88 88 88b d88 88 `88. 88 8D 88 88 88 88.")
|
if c.String("username") == "" {
|
||||||
fmt.Println(" `Y88P' YP YP YP YP YP ~Y8888P' 88 YD Y8888P' YP YP YP Y88888P")
|
log.Fatal(errNoUsername)
|
||||||
fmt.Println("d8888b. db db d8888b.")
|
}
|
||||||
fmt.Println("88 `8D 88 88 88 `8D")
|
// Converts `strip` from MiB to Bytes
|
||||||
fmt.Println("88 88 Y8 8P 88oobY'")
|
stripLimit = c.Int("strip") * 1024 * 1024
|
||||||
fmt.Println("88 88 `8b d8' 88`8b")
|
stripQuota = c.Int("strip") * 1024 * 1024
|
||||||
fmt.Println("88 .8D `8bd8' 88 `88.")
|
//
|
||||||
fmt.Println("Y8888D' YP 88 YD")
|
|
||||||
fmt.Println("---")
|
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'")
|
||||||
// Mkdir video folder
|
fmt.Println("8P 88ooo88 88ooo88 88 88 88 88oobY' 88oooY' 88ooo88 88 88ooooo")
|
||||||
if _, err := os.Stat("./" + savePath); os.IsNotExist(err) {
|
fmt.Println("8b 88~~~88 88~~~88 88 88 88 88`8b 88~~~b. 88~~~88 88 88~~~~~")
|
||||||
os.Mkdir("./"+savePath, 0777)
|
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.")
|
||||||
if c.Int("strip") != 0 {
|
fmt.Println("88 `8D 88 88 88 `8D")
|
||||||
log.Printf("specifying stripping limit as %d MiB(s)", c.Int("strip"))
|
fmt.Println("88 88 Y8 8P 88oobY'")
|
||||||
}
|
fmt.Println("88 88 `8b d8' 88`8b")
|
||||||
|
fmt.Println("88 .8D `8bd8' 88 `88.")
|
||||||
for {
|
fmt.Println("Y8888D' YP 88 YD")
|
||||||
// Capture the stream if the user is currently online.
|
fmt.Println("---")
|
||||||
if getOnlineStatus(c.String("username")) {
|
|
||||||
log.Printf("%s is online! fetching...", c.String("username"))
|
// Mkdir video folder
|
||||||
capture(c.String("username"))
|
if _, err := os.Stat("./" + savePath); os.IsNotExist(err) {
|
||||||
segmentIndex = 0
|
os.Mkdir("./"+savePath, 0777)
|
||||||
bucket = []string{}
|
}
|
||||||
retriesAfterOnlined = 0
|
//
|
||||||
continue
|
if c.Int("strip") != 0 {
|
||||||
}
|
log.Printf("specifying stripping limit as %d MiB(s)", c.Int("strip"))
|
||||||
// Otherwise we keep checking the channel status until the user is online.
|
}
|
||||||
log.Printf("%s is not online, check again after %d minute(s)...", c.String("username"), c.Int("interval"))
|
|
||||||
<-time.After(time.Minute * time.Duration(c.Int("interval")))
|
for {
|
||||||
}
|
// Capture the stream if the user is currently online.
|
||||||
}
|
if getOnlineStatus(c.String("username")) {
|
||||||
|
log.Printf(infoIsOnline, c.String("username"))
|
||||||
func main() {
|
capture(c.String("username"))
|
||||||
app := &cli.App{
|
segmentIndex = 0
|
||||||
Flags: []cli.Flag{
|
temp = []string{}
|
||||||
&cli.StringFlag{
|
retriesAfterOnlined = 0
|
||||||
Name: "username",
|
continue
|
||||||
Aliases: []string{"u"},
|
}
|
||||||
Value: "",
|
// Otherwise we keep checking the channel status until the user is online.
|
||||||
Usage: "channel username to watching",
|
log.Printf(infoNotOnline, c.String("username"), c.Int("interval"))
|
||||||
},
|
<-time.After(time.Minute * time.Duration(c.Int("interval")))
|
||||||
&cli.IntFlag{
|
}
|
||||||
Name: "interval",
|
}
|
||||||
Aliases: []string{"i"},
|
|
||||||
Value: 1,
|
func main() {
|
||||||
Usage: "minutes to check if a channel goes online or not",
|
app := &cli.App{
|
||||||
},
|
Flags: []cli.Flag{
|
||||||
&cli.IntFlag{
|
&cli.StringFlag{
|
||||||
Name: "strip",
|
Name: "username",
|
||||||
Aliases: []string{"s"},
|
Aliases: []string{"u"},
|
||||||
Value: 0,
|
Value: "",
|
||||||
Usage: "MB sizes to split the video into chunks",
|
Usage: "channel username to watching",
|
||||||
},
|
},
|
||||||
},
|
&cli.IntFlag{
|
||||||
Name: "chaturbate-dvr",
|
Name: "interval",
|
||||||
Usage: "watching a specified chaturbate channel and auto saves the stream as local file",
|
Aliases: []string{"i"},
|
||||||
Action: endpoint,
|
Value: 1,
|
||||||
}
|
Usage: "minutes to check if a channel goes online or not",
|
||||||
err := app.Run(os.Args)
|
},
|
||||||
if err != nil {
|
&cli.IntFlag{
|
||||||
log.Fatal(err)
|
Name: "strip",
|
||||||
}
|
Aliases: []string{"s"},
|
||||||
}
|
Value: 0,
|
||||||
|
Usage: "MB sizes to split the video into chunks",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user