From 3bdae1b872c6f8730f3ffeb389cb1a98d1c52c2e Mon Sep 17 00:00:00 2001 From: J0nDoe Date: Tue, 22 Oct 2024 15:04:53 -0400 Subject: [PATCH] Updates and refactors Channel_file.go - fix issue with segments not correctly ending when they were supposed to Log_type.go - moved log type to it's own file, setup global logging (touches on issue #47) Main.go - added update_log_level handler, setting global log level Channel.go, channel_internal.go, channel_util.go - updated to use new log_type Manager.go - updated to use new log_type, update from .com to .global (issue #74) Channel_update.go, create_channel.go, delete_channel.go, get_channel.go, get_settings.go, listen_update.go, pause_channel.go, resume_channel.go, terminal_program.go - go fmt / go vet Chaturbate_channels.json.sample - added sample json of the channels file, for mapping in docker config List_channels.go - refactored to sort by online status, so online is always at the first ones you see Script.js - adjust default settings, added pagination, added global log logic Index.html - updated to use online version of tocas ui, added pagination, added global log logic, visual improvements Removal of local tocas folder since using online version --- chaturbate/channel.go | 20 +- chaturbate/channel_file.go | 60 +-- chaturbate/channel_internal.go | 68 +-- chaturbate/channel_util.go | 33 +- chaturbate/log_type.go | 64 +++ chaturbate/manager.go | 6 +- chaturbate_channels.json.sample | 12 + go.mod | 28 +- go.sum | 63 ++- handler/list_channels.go | 13 + handler/update_log_level.go | 120 ++++++ handler/view/index.html | 737 ++++++++++++++++---------------- handler/view/script.js | 462 +++++++++++--------- main.go | 9 +- 14 files changed, 998 insertions(+), 697 deletions(-) create mode 100644 chaturbate/log_type.go create mode 100644 chaturbate_channels.json.sample create mode 100644 handler/update_log_level.go diff --git a/chaturbate/channel.go b/chaturbate/channel.go index 7ce4ea3..20f2c8a 100644 --- a/chaturbate/channel.go +++ b/chaturbate/channel.go @@ -38,7 +38,7 @@ type Channel struct { IsPaused bool isStopped bool Logs []string - logType logType + LogType LogType bufferLock sync.Mutex buffer map[int][]byte @@ -61,32 +61,32 @@ type Channel struct { // Run func (w *Channel) Run() { if w.Username == "" { - w.log(logTypeError, "username is empty, use `-u USERNAME` to specify") + w.log(LogTypeError, "username is empty, use `-u USERNAME` to specify") return } for { if w.IsPaused { - w.log(logTypeInfo, "channel is paused") + w.log(LogTypeInfo, "channel is paused") <-w.ResumeChannel // blocking - w.log(logTypeInfo, "channel is resumed") + w.log(LogTypeInfo, "channel is resumed") } if w.isStopped { - w.log(logTypeInfo, "channel is stopped") + w.log(LogTypeInfo, "channel is stopped") break } body, err := w.requestChannelBody() if err != nil { - w.log(logTypeError, "body request error: %w", err) + w.log(LogTypeError, "body request error: %v", err) } if strings.Contains(body, "playlist.m3u8") { w.IsOnline = true w.LastStreamedAt = time.Now().Format("2006-01-02 15:04:05") - w.log(logTypeInfo, "channel is online, start fetching...") + w.log(LogTypeInfo, "channel is online, start fetching...") if err := w.record(body); err != nil { // blocking - w.log(logTypeError, "record error: %w", err) + w.log(LogTypeError, "record error: %v", err) } continue // this excutes when recording is over/interrupted } @@ -95,11 +95,11 @@ func (w *Channel) Run() { // close file when offline so user can move/delete it if w.file != nil { if err := w.releaseFile(); err != nil { - w.log(logTypeError, "release file: %w", err) + w.log(LogTypeError, "release file: %v", err) } } - w.log(logTypeInfo, "channel is offline, check again %d min(s) later", w.Interval) + w.log(LogTypeInfo, "channel is offline, check again %d min(s) later", w.Interval) <-time.After(time.Duration(w.Interval) * time.Minute) // minutes cooldown to check online status } } diff --git a/chaturbate/channel_file.go b/chaturbate/channel_file.go index 36ab8e3..21f27c6 100644 --- a/chaturbate/channel_file.go +++ b/chaturbate/channel_file.go @@ -9,11 +9,10 @@ import ( "time" ) -// filename +// filename generates the filename based on the session pattern and current split index. func (w *Channel) filename() (string, error) { - data := w.sessionPattern - if data == nil { - data = map[string]any{ + if w.sessionPattern == nil { + w.sessionPattern = map[string]any{ "Username": w.Username, "Year": time.Now().Format("2006"), "Month": time.Now().Format("01"), @@ -23,69 +22,82 @@ func (w *Channel) filename() (string, error) { "Second": time.Now().Format("05"), "Sequence": 0, } - w.sessionPattern = data - } else { - data["Sequence"] = w.splitIndex } - t, err := template.New("filename").Parse(w.filenamePattern) + + w.sessionPattern["Sequence"] = w.splitIndex + + var buf bytes.Buffer + tmpl, err := template.New("filename").Parse(w.filenamePattern) if err != nil { - return "", err + return "", fmt.Errorf("filename pattern error: %w", err) } - var buf bytes.Buffer - if err := t.Execute(&buf, data); err != nil { - return "", err + if err := tmpl.Execute(&buf, w.sessionPattern); err != nil { + return "", fmt.Errorf("template execution error: %w", err) } + return buf.String(), nil } -// newFile +// newFile creates a new file and prepares it for writing stream data. func (w *Channel) newFile() error { filename, err := w.filename() if err != nil { - return fmt.Errorf("filename pattern error: %w", err) + return err } + if err := os.MkdirAll(filepath.Dir(filename), 0777); err != nil { return fmt.Errorf("create folder: %w", err) } + file, err := os.OpenFile(filename+".ts", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0777) if err != nil { return fmt.Errorf("cannot open file: %s: %w", filename, err) } - w.log(logTypeInfo, "the stream will be saved as %s.ts", filename) + + w.log(LogTypeInfo, "the stream will be saved as %s.ts", filename) w.file = file return nil } -// releaseFile +// releaseFile closes the current file and removes it if empty. func (w *Channel) releaseFile() error { if w.file == nil { return nil } - // close the file to remove it + if err := w.file.Close(); err != nil { return fmt.Errorf("close file: %s: %w", w.file.Name(), err) } - // remove it if it was empty - if w.SegmentFilesize == 0 { - w.log(logTypeInfo, "%s was removed because it was empty", w.file.Name()) + if w.SegmentFilesize == 0 { + w.log(LogTypeInfo, "%s was removed because it was empty", w.file.Name()) if err := os.Remove(w.file.Name()); err != nil { return fmt.Errorf("remove zero file: %s: %w", w.file.Name(), err) } } + w.file = nil return nil } -// nextFile -func (w *Channel) nextFile() error { +// nextFile handles the transition to a new file segment, ensuring correct timing. +func (w *Channel) nextFile(startTime time.Time) error { + // Release the current file before creating a new one. if err := w.releaseFile(); err != nil { - w.log(logTypeError, "release file: %w", err) + w.log(LogTypeError, "release file: %v", err) + return err } + // Increment the split index for the next file. w.splitIndex++ + + // Reset segment data. w.SegmentFilesize = 0 - w.SegmentDuration = 0 + // Calculate the actual segment duration using the elapsed time. + elapsed := int(time.Since(startTime).Minutes()) + w.SegmentDuration = elapsed + + // Create the new file. return w.newFile() } diff --git a/chaturbate/channel_internal.go b/chaturbate/channel_internal.go index 1fcd595..0d2c09f 100644 --- a/chaturbate/channel_internal.go +++ b/chaturbate/channel_internal.go @@ -175,17 +175,17 @@ func (w *Channel) resolveSource(body string) (string, string, error) { if variant == nil { return "", "", fmt.Errorf("no available resolution") } - w.log(logTypeInfo, "resolution %dp is used", variant.width) + w.log(LogTypeInfo, "resolution %dp is used", variant.width) url, ok := variant.framerate[w.Framerate] // If the framerate is not found, fallback to the first found framerate, this block pretends there're only 30 and 60 fps. // no complex logic here, im lazy. if ok { - w.log(logTypeInfo, "framerate %dfps is used", w.Framerate) + w.log(LogTypeInfo, "framerate %dfps is used", w.Framerate) } else { for k, v := range variant.framerate { url = v - w.log(logTypeWarning, "framerate %dfps not found, fallback to %dfps", w.Framerate, k) + w.log(LogTypeWarning, "framerate %dfps not found, fallback to %dfps", w.Framerate, k) w.Framerate = k break } @@ -196,66 +196,86 @@ func (w *Channel) resolveSource(body string) (string, string, error) { return rootURL, sourceURL, nil } -// mergeSegments is a async function that runs in background for the channel, -// and it merges the segments from buffer to the file. +// mergeSegments runs in the background and merges segments from the buffer to the file. func (w *Channel) mergeSegments() { var segmentRetries int + startTime := time.Now() // Track the start time of the current segment. for { if w.IsPaused || w.isStopped { break } + + // Handle segment retries if not found. if segmentRetries > 5 { - w.log(logTypeWarning, "segment #%d not found in buffer, skipped", w.bufferIndex) + w.log(LogTypeWarning, "segment #%d not found in buffer, skipped", w.bufferIndex) w.bufferIndex++ segmentRetries = 0 continue } + + // If buffer is empty, wait and retry. if len(w.buffer) == 0 { - <-time.After(1 * time.Second) + time.Sleep(1 * time.Second) continue } + + // Retrieve segment from buffer. w.bufferLock.Lock() buf, ok := w.buffer[w.bufferIndex] w.bufferLock.Unlock() + if !ok { segmentRetries++ - <-time.After(time.Duration(segmentRetries) * time.Second) + time.Sleep(time.Duration(segmentRetries) * time.Second) continue } + + // Write the segment to the file. lens, err := w.file.Write(buf) if err != nil { - w.log(logTypeError, "segment #%d written error: %v", w.bufferIndex, err) + w.log(LogTypeError, "segment #%d written error: %v", w.bufferIndex, err) w.retries++ continue } - w.log(logTypeInfo, "segment #%d written", w.bufferIndex) - w.log(logTypeDebug, "duration: %s, size: %s", DurationStr(w.SegmentDuration), ByteStr(w.SegmentFilesize)) + // Update segment size and log progress. w.SegmentFilesize += lens - segmentRetries = 0 + w.log(LogTypeInfo, "segment #%d written", w.bufferIndex) + w.log(LogTypeDebug, "duration: %s, size: %s", DurationStr(w.SegmentDuration), ByteStr(w.SegmentFilesize)) + // Check if the file size limit has been reached. if w.SplitFilesize > 0 && w.SegmentFilesize >= w.SplitFilesize*1024*1024 { - w.log(logTypeInfo, "filesize exceeded, creating new file") + w.log(LogTypeInfo, "filesize exceeded, creating new file") - if err := w.nextFile(); err != nil { - w.log(logTypeError, "next file error: %v", err) + if err := w.nextFile(startTime); err != nil { + w.log(LogTypeError, "next file error: %v", err) break } - } else if w.SplitDuration > 0 && w.SegmentDuration >= w.SplitDuration*60 { - w.log(logTypeInfo, "duration exceeded, creating new file") - if err := w.nextFile(); err != nil { - w.log(logTypeError, "next file error: %v", err) + startTime = time.Now() // Reset start time for the new segment. + } + + // Check if the duration limit has been reached. + elapsed := int(time.Since(startTime).Minutes()) + if w.SplitDuration > 0 && elapsed >= w.SplitDuration { + w.log(LogTypeInfo, "duration exceeded, creating new file") + + if err := w.nextFile(startTime); err != nil { + w.log(LogTypeError, "next file error: %v", err) break } + + startTime = time.Now() // Reset start time for the new segment. } + // Remove the processed segment from the buffer. w.bufferLock.Lock() delete(w.buffer, w.bufferIndex) w.bufferLock.Unlock() - w.bufferIndex++ + w.bufferIndex++ // Move to the next segment. + segmentRetries = 0 // Reset retries for the next segment. } } @@ -276,7 +296,7 @@ func (w *Channel) fetchSegments() { break } - w.log(logTypeError, "segment list error, will try again [%d/10]: %v", disconnectRetries, err) + w.log(LogTypeError, "segment list error, will try again [%d/10]: %v", disconnectRetries, err) disconnectRetries++ <-time.After(time.Duration(wait) * time.Second) @@ -284,7 +304,7 @@ func (w *Channel) fetchSegments() { } if disconnectRetries > 0 { - w.log(logTypeInfo, "channel is back online!") + w.log(LogTypeInfo, "channel is back online!") w.IsOnline = true disconnectRetries = 0 } @@ -296,7 +316,7 @@ func (w *Channel) fetchSegments() { go func(index int, uri string) { if err := w.requestSegment(uri, index); err != nil { - w.log(logTypeError, "segment #%d request error, ignored: %v", index, err) + w.log(LogTypeError, "segment #%d request error, ignored: %v", index, err) return } }(w.segmentIndex, v.URI) @@ -379,7 +399,7 @@ func (w *Channel) requestSegment(url string, index int) error { return fmt.Errorf("read body: %w", err) } - w.log(logTypeDebug, "segment #%d fetched", index) + w.log(LogTypeDebug, "segment #%d fetched", index) w.bufferLock.Lock() w.buffer[index] = body diff --git a/chaturbate/channel_util.go b/chaturbate/channel_util.go index d44ca47..f886ff9 100644 --- a/chaturbate/channel_util.go +++ b/chaturbate/channel_util.go @@ -5,34 +5,28 @@ import ( "time" ) -type logType string - -const ( - logTypeDebug logType = "DEBUG" - logTypeInfo logType = "INFO" - logTypeWarning logType = "WARN" - logTypeError logType = "ERROR" -) - // log -func (w *Channel) log(typ logType, message string, v ...interface{}) { - switch w.logType { - case logTypeInfo: - if typ == logTypeDebug { +func (w *Channel) log(typ LogType, message string, v ...interface{}) { + // Check the global log level + currentLogLevel := GetGlobalLogLevel() + + switch currentLogLevel { + case LogTypeInfo: + if typ == LogTypeDebug { return } - case logTypeWarning: - if typ == logTypeDebug || typ == logTypeInfo { + case LogTypeWarning: + if typ == LogTypeDebug || typ == LogTypeInfo { return } - case logTypeError: - if typ == logTypeDebug || typ == logTypeInfo || typ == logTypeWarning { + case LogTypeError: + if typ == LogTypeDebug || typ == LogTypeInfo || typ == LogTypeWarning { return } } - updateLog := fmt.Sprintf("[%s] [%s] %s", time.Now().Format("2006-01-02 15:04:05"), typ, fmt.Errorf(message, v...)) - consoleLog := fmt.Sprintf("[%s] [%s] [%s] %s", time.Now().Format("2006-01-02 15:04:05"), typ, w.Username, fmt.Errorf(message, v...)) + updateLog := fmt.Sprintf("[%s] [%s] %s", time.Now().Format("2006-01-02 15:04:05"), typ, fmt.Sprintf(message, v...)) + consoleLog := fmt.Sprintf("[%s] [%s] [%s] %s", time.Now().Format("2006-01-02 15:04:05"), typ, w.Username, fmt.Sprintf(message, v...)) update := &Update{ Username: w.Username, @@ -43,6 +37,7 @@ func (w *Channel) log(typ logType, message string, v ...interface{}) { SegmentDuration: w.SegmentDuration, SegmentFilesize: w.SegmentFilesize, } + if w.file != nil { update.Filename = w.file.Name() } diff --git a/chaturbate/log_type.go b/chaturbate/log_type.go new file mode 100644 index 0000000..5c4d783 --- /dev/null +++ b/chaturbate/log_type.go @@ -0,0 +1,64 @@ +package chaturbate + +import ( + "encoding/json" + "fmt" + "strings" + "sync" +) + +type LogType string + +type LogLevelRequest struct { + LogLevel LogType `json:"log_level" binding:"required"` +} + +// Define the log types +const ( + LogTypeDebug LogType = "DEBUG" + LogTypeInfo LogType = "INFO" + LogTypeWarning LogType = "WARN" + LogTypeError LogType = "ERROR" +) + +// Global log level with mutex protection +var ( + globalLogLevel LogType + logMutex sync.RWMutex // Protects global log level access +) + +// UnmarshalJSON ensures that LogType is properly parsed from JSON. +func (l *LogType) UnmarshalJSON(b []byte) error { + var s string + if err := json.Unmarshal(b, &s); err != nil { + return err + } + + parsed := LogType(strings.ToUpper(s)) + switch parsed { + case LogTypeDebug, LogTypeInfo, LogTypeWarning, LogTypeError: + *l = parsed + return nil + default: + return fmt.Errorf("invalid log level: %s", s) + } +} + +// InitGlobalLogLevel initializes the global log level from settings. +func InitGlobalLogLevel(initialLevel LogType) { + SetGlobalLogLevel(initialLevel) +} + +// SetGlobalLogLevel updates the global log level +func SetGlobalLogLevel(level LogType) { + logMutex.Lock() + defer logMutex.Unlock() + globalLogLevel = level +} + +// GetGlobalLogLevel retrieves the current global log level +func GetGlobalLogLevel() LogType { + logMutex.RLock() + defer logMutex.RUnlock() + return globalLogLevel +} diff --git a/chaturbate/manager.go b/chaturbate/manager.go index 6c36f10..f9dc85d 100644 --- a/chaturbate/manager.go +++ b/chaturbate/manager.go @@ -95,7 +95,7 @@ func (m *Manager) CreateChannel(conf *Config) error { } c := &Channel{ Username: conf.Username, - ChannelURL: "https://chaturbate.com/" + conf.Username, + ChannelURL: "https://chaturbate.global/" + conf.Username, filenamePattern: conf.FilenamePattern, Framerate: conf.Framerate, Resolution: conf.Resolution, @@ -112,7 +112,7 @@ func (m *Manager) CreateChannel(conf *Config) error { Logs: []string{}, UpdateChannel: make(chan *Update), ResumeChannel: make(chan bool), - logType: logType(m.cli.String("log-level")), + LogType: LogType(m.cli.String("log-level")), } go func() { for update := range c.UpdateChannel { @@ -124,7 +124,7 @@ func (m *Manager) CreateChannel(conf *Config) error { } }() m.Channels[conf.Username] = c - c.log(logTypeInfo, "channel created") + c.log(LogTypeInfo, "channel created") go c.Run() return nil } diff --git a/chaturbate_channels.json.sample b/chaturbate_channels.json.sample new file mode 100644 index 0000000..b64f449 --- /dev/null +++ b/chaturbate_channels.json.sample @@ -0,0 +1,12 @@ +[ + { + "Username": "", + "FilenamePattern": "videos/{{.Username}}/{{.Year}}-{{.Month}}-{{.Day}}/{{.Username}}_{{.Hour}}-{{.Minute}}-{{.Second}}{{if .Sequence}}_{{.Sequence}}{{end}}", + "Framerate": 30, + "Resolution": 1080, + "ResolutionFallback": "down", + "SplitDuration": 30, + "SplitFilesize": 0, + "Interval": 1 + } +] \ No newline at end of file diff --git a/go.mod b/go.mod index e4c5a13..ab9095a 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.22.0 require ( github.com/gin-gonic/gin v1.9.1 + github.com/go-playground/validator/v10 v10.19.0 github.com/google/uuid v1.5.0 github.com/grafov/m3u8 v0.12.0 github.com/samber/lo v1.39.0 @@ -11,36 +12,35 @@ require ( ) require ( - github.com/bytedance/sonic v1.10.1 // indirect + github.com/bytedance/sonic v1.11.3 // indirect github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect - github.com/chenzhuoyu/iasm v0.9.0 // indirect + github.com/chenzhuoyu/iasm v0.9.1 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect - github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // 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 - github.com/go-playground/validator/v10 v10.15.5 // indirect 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/klauspost/cpuid/v2 v2.2.7 // 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/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // 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/pelletier/go-toml/v2 v2.2.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 + github.com/ugorji/go/codec v1.2.12 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect - golang.org/x/arch v0.5.0 // indirect - golang.org/x/crypto v0.18.0 // indirect + golang.org/x/arch v0.7.0 // indirect + golang.org/x/crypto v0.22.0 // indirect golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect - golang.org/x/net v0.20.0 // indirect - golang.org/x/sys v0.16.0 // indirect + golang.org/x/net v0.24.0 // indirect + golang.org/x/sys v0.19.0 // indirect golang.org/x/text v0.14.0 // indirect - google.golang.org/protobuf v1.31.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 6043074..cfee07f 100644 --- a/go.sum +++ b/go.sum @@ -1,21 +1,22 @@ github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= 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/bytedance/sonic v1.11.3 h1:jRN+yEjakWh8aK5FzrciUHG8OFXK+4/KrAX/ysEtHAA= +github.com/bytedance/sonic v1.11.3/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/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= -github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo= github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= +github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0= +github.com/chenzhuoyu/iasm v0.9.1/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/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= 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= @@ -26,11 +27,10 @@ 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.15.5 h1:LEBecTWb/1j5TNY1YYG2RcOUN3R7NLylN+x8TTueE24= -github.com/go-playground/validator/v10 v10.15.5/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4= +github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -41,8 +41,8 @@ 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.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= -github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/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= @@ -52,17 +52,17 @@ 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= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 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.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= -github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pelletier/go-toml/v2 v2.2.0 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeBAsoHo= +github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= 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= @@ -76,43 +76,42 @@ github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXn github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 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= -github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= -github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho= github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= 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.5.0 h1:jpGode6huXQxcskEIpOCvrU+tzo81b6+oFLUYXWtH/Y= -golang.org/x/arch v0.5.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc= +golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= 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.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= 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.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -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.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 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= diff --git a/handler/list_channels.go b/handler/list_channels.go index 87be69f..174e1fd 100644 --- a/handler/list_channels.go +++ b/handler/list_channels.go @@ -2,6 +2,7 @@ package handler import ( "net/http" + "sort" "github.com/gin-gonic/gin" "github.com/teacat/chaturbate-dvr/chaturbate" @@ -51,17 +52,27 @@ func NewListChannelsHandler(c *chaturbate.Manager, cli *cli.Context) *ListChanne // Handle //======================================================= +// Handle processes the request to list channels, sorting by IsOnline. func (h *ListChannelsHandler) Handle(c *gin.Context) { var req *ListChannelsRequest if err := c.ShouldBindJSON(&req); err != nil { c.AbortWithError(http.StatusBadRequest, err) return } + + // Fetch channels channels, err := h.chaturbate.ListChannels() if err != nil { c.AbortWithError(http.StatusInternalServerError, err) return } + + // Sort by IsOnline: online channels first, then offline + sort.SliceStable(channels, func(i, j int) bool { + return channels[i].IsOnline && !channels[j].IsOnline + }) + + // Populate response resp := &ListChannelsResponse{ Channels: make([]*ListChannelsResponseChannel, len(channels)), } @@ -81,5 +92,7 @@ func (h *ListChannelsHandler) Handle(c *gin.Context) { Logs: channel.Logs, } } + + // Send the response c.JSON(http.StatusOK, resp) } diff --git a/handler/update_log_level.go b/handler/update_log_level.go new file mode 100644 index 0000000..03767c5 --- /dev/null +++ b/handler/update_log_level.go @@ -0,0 +1,120 @@ +package handler + +import ( + "log" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" + "github.com/go-playground/validator/v10" + "github.com/teacat/chaturbate-dvr/chaturbate" + "github.com/urfave/cli/v2" +) + +type UpdateLogLevelHandler struct { + cli *cli.Context +} + +// Custom validator for LogType +func LogTypeValidator(fl validator.FieldLevel) bool { + value := fl.Field().String() + switch value { + case string(chaturbate.LogTypeDebug), string(chaturbate.LogTypeInfo), string(chaturbate.LogTypeWarning), string(chaturbate.LogTypeError): + return true + } + return false +} + +func init() { + if v, ok := binding.Validator.Engine().(*validator.Validate); ok { + v.RegisterValidation("logtype", LogTypeValidator) + } +} + +func NewUpdateLogLevelHandler(cli *cli.Context) *UpdateLogLevelHandler { + return &UpdateLogLevelHandler{cli} +} + +func (h *UpdateLogLevelHandler) Handle(c *gin.Context) { + var req chaturbate.LogLevelRequest + + // Bind and validate the request body + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Invalid request format. Expected {\"log_level\": \"INFO\"}", + }) + return + } + + // Use the correct log type for setting the global log level + chaturbate.SetGlobalLogLevel(req.LogLevel) + + log.Printf("Global log level updated to: %s", req.LogLevel) + + // Send success response + c.JSON(http.StatusOK, gin.H{ + "message": "Log level updated", + "log_level": req.LogLevel, + }) +} + +// func (h *UpdateLogLevelHandler) Handle(c *gin.Context) { +// // Read the raw request body for debugging +// bodyBytes, err := c.GetRawData() +// if err != nil { +// log.Printf("Error reading request body: %v", err) +// c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request format"}) +// return +// } + +// // Log the raw request body +// log.Printf("Received raw request body: %s", string(bodyBytes)) + +// // Reset the request body so it can be re-read by ShouldBindJSON +// c.Request.Body = ioutil.NopCloser(strings.NewReader(string(bodyBytes))) + +// // Attempt to bind the JSON to the struct +// var req LogLevelRequest +// if err := c.ShouldBindJSON(&req); err != nil { +// log.Printf("Error binding JSON: %v", err) +// c.JSON(http.StatusBadRequest, gin.H{ +// "error": "Invalid request format. Expected {\"log_level\": \"INFO\"}", +// }) +// return +// } + +// // Log the updated log level +// log.Printf("Log level updated to: %s", req.LogLevel) + +// // Store the log level in the CLI context if needed +// h.cli.Set("log_level", string(req.LogLevel)) + +// // Send success response +// c.JSON(http.StatusOK, gin.H{ +// "message": "Log level updated", +// "log_level": req.LogLevel, +// }) +// } + +// NewUpdateLogLevelHandler creates a handler for updating log level. +// func NewUpdateLogLevelHandler(c *cli.Context) gin.HandlerFunc { +// return func(ctx *gin.Context) { +// var req LogLevelRequest + +// // Bind and validate request body +// if err := ctx.ShouldBindJSON(&req); err != nil { +// ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request format"}) +// return +// } + +// if !allowedLogLevels[req.LogLevel] { +// ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid log level"}) +// return +// } + +// ctx.JSON(http.StatusOK, gin.H{ +// "message": "Log level updated", +// "log_level": req.LogLevel, +// }) +// } +// } diff --git a/handler/view/index.html b/handler/view/index.html index 6e7cc4c..d1c17e6 100644 --- a/handler/view/index.html +++ b/handler/view/index.html @@ -1,391 +1,410 @@ - - - - - - - - - - - Chaturbate DVR - - - - -
- -
-
-
-
Add Channel
-
-
- -
-
-
- + + + + + + + + + + + Chaturbate DVR + + + + +
+ +
+
+
+
Add Channel
+
+
+ +
+
+
+ -
+
- -
- -
-
Channel Username
-
-
-
chaturbate.com/
- -
-
Use commas to separate multiple channel names. For example, "channel1,channel2,channel3".
-
-
- + +
+ +
+
Channel Username
+
+
+
chaturbate.global/
+ +
+
Use commas to separate multiple channel names. For example, "channel1,channel2,channel3".
+
+
+ - -
-
Resolution
-
-
-
-
- -
-
-
-
- -
-
-
-
- The resolution will be used if - was not available. -
-
-
- + +
+
Resolution
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+ The resolution will be used if + was not available. +
+
+
+ - -
-
Framerate
-
-
- - -
- -
-
- + +
+
Framerate
+
+
+ + +
+ +
+
+ - -
-
Filename Pattern
-
-
- -
-
- See the README for details. -
-
-
- + +
+
Filename Pattern
+
+
+ +
+
See the README for details.
+
+
+ - - - + + + -
+
- -
-
-
-
- Splitting Options -
-
-
-
by Filesize
-
- - MB -
-
-
-
by Duration
-
- - Mins -
-
-
-
Splitting will be disabled if both options are 0.
-
-
-
+ +
+
+
+
+ Splitting Options +
+
+
+
by Filesize
+
+ + MB +
+
+
+
by Duration
+
+ + Mins +
- +
+
Splitting will be disabled if both options are 0.
- +
+
+
+ +
+ -
+
- -
-
- - -
-
- + +
+
+ + +
+
+ +
+
+ + + +
+ +
+
+
Chaturbate DVR
+
Version
+
+
+
+
+ +
+ + +
+
+
+ + + + + - -
- -
+ + + + +
+ « + + » +
+ +
+ + + +
+
+ + diff --git a/handler/view/script.js b/handler/view/script.js index ba59a7f..7141b09 100644 --- a/handler/view/script.js +++ b/handler/view/script.js @@ -1,212 +1,254 @@ function data() { - return { - settings: {}, - channels: [], - is_updating_channels: false, - form_data: { - username: "", - resolution: "1080", - resolution_fallback: "up", - framerate: "30", - filename_pattern: "{{.Username}}_{{.Year}}-{{.Month}}-{{.Day}}_{{.Hour}}-{{.Minute}}-{{.Second}}{{if .Sequence}}_{{.Sequence}}{{end}}", - split_filesize: 0, - split_duration: 0, - interval: 1, - }, - - // openCreateDialog - openCreateDialog() { - document.getElementById("create-dialog").showModal() - }, - - // closeCreateDialog - closeCreateDialog() { - document.getElementById("create-dialog").close() - this.resetCreateDialog() - }, - - // submitCreateDialog - submitCreateDialog() { - this.createChannel() - this.closeCreateDialog() - }, - - // error - error() { - alert("Error occurred, please refresh the page if something is wrong.") - }, - - // - async call(path, body) { - try { - var resp = await fetch(`/api/${path}`, { - body: JSON.stringify(body), - method: "POST", - }) - if (resp.status !== 200) { - this.error() - return [null, true] - } - return [await resp.json(), false] - } catch { - this.error() - return [null, true] - } - }, - - // getSettings - async getSettings() { - var [resp, err] = await this.call("get_settings", {}) - if (!err) { - this.settings = resp - this.resetCreateDialog() - } - }, - - // init - async init() { - document.getElementById("create-dialog").addEventListener("close", () => this.resetCreateDialog()) - - await this.getSettings() - await this.listChannels() - this.listenUpdate() - }, - - // resetCreateDialog - resetCreateDialog() { - document.getElementById("splitting-accordion").open = false - - this.form_data = { - username: "", - resolution: this.settings.resolution.toString(), - resolution_fallback: this.settings.resolution_fallback, - framerate: this.settings.framerate.toString(), - filename_pattern: this.settings.filename_pattern, - split_filesize: this.settings.split_filesize.toString(), - split_duration: this.settings.split_duration.toString(), - interval: this.settings.interval.toString(), - } - }, - - // createChannel - async createChannel() { - await this.call("create_channel", { - username: this.form_data.username, - resolution: parseInt(this.form_data.resolution), - resolution_fallback: this.form_data.resolution_fallback, - framerate: parseInt(this.form_data.framerate), - filename_pattern: this.form_data.filename_pattern, - split_filesize: parseInt(this.form_data.split_filesize), - split_duration: parseInt(this.form_data.split_duration), - interval: parseInt(this.form_data.interval), - }) - }, - - // deleteChannel - async deleteChannel(username) { - if (!confirm(`Are you sure you want to delete the channel "${username}"?`)) { - return - } - var [_, err] = await this.call("delete_channel", { username }) - if (!err) { - this.channels = this.channels.filter(ch => ch.username !== username) - } - }, - - // pauseChannel - async pauseChannel(username) { - await this.call("pause_channel", { username }) - }, - - // terminateProgram - async terminateProgram() { - if (confirm("Are you sure you want to terminate the program?")) { - alert("The program is terminated, any error messages are safe to ignore.") - await this.call("terminate_program", {}) - } - }, - - // resumeChannel - async resumeChannel(username) { - await this.call("resume_channel", { username }) - }, - - // listChannels - async listChannels() { - if (this.is_updating_channels) { - return - } - var [resp, err] = await this.call("list_channels", {}) - if (!err) { - this.channels = resp.channels - this.channels.forEach(ch => { - this.scrollLogs(ch.username) - }) - } - this.is_updating_channels = false - }, - - // listenUpdate - listenUpdate() { - var source = new EventSource("/api/listen_update") - - source.onmessage = event => { - var data = JSON.parse(event.data) - - // If the channel is not in the list or is stopped, refresh the list. - if (!this.channels.some(ch => ch.username === data.username) || data.is_stopped) { - this.listChannels() - return - } - - var index = this.channels.findIndex(ch => ch.username === data.username) - - if (index === -1) { - return - } - - this.channels[index].segment_duration = data.segment_duration - this.channels[index].segment_filesize = data.segment_filesize - this.channels[index].filename = data.filename - this.channels[index].last_streamed_at = data.last_streamed_at - this.channels[index].is_online = data.is_online - this.channels[index].is_paused = data.is_paused - this.channels[index].logs = [...this.channels[index].logs, data.log] - - if (this.channels[index].logs.length > 100) { - this.channels[index].logs = this.channels[index].logs.slice(-100) - } - - this.scrollLogs(data.username) - } - - source.onerror = err => { - source.close() - } - }, - - downloadLogs(username) { - var a = window.document.createElement("a") - a.href = window.URL.createObjectURL( - new Blob([this.channels[this.channels.findIndex(ch => ch.username === username)].logs.join("\n")], { type: "text/plain", oneTimeOnly: true }) - ) - a.download = `${username}_logs.txt` - document.body.appendChild(a) - a.click() - document.body.removeChild(a) - }, - - // - scrollLogs(username) { - // Wait for the DOM to update. - setTimeout(() => { - var logs_element = document.getElementById(`${username}-logs`) - - if (!logs_element) { - return - } - logs_element.scrollTop = logs_element.scrollHeight - }, 1) - }, - } + return { + settings: {}, + channels: [], + currentPage: 1, + itemsPerPage: 5, + is_updating_channels: false, + form_data: { + username: "", + resolution: "1080", + resolution_fallback: "down", + framerate: "30", + filename_pattern: "{{.Username}}/{{.Year}}-{{.Month}}-{{.Day}}/{{.Username}}_{{.Hour}}-{{.Minute}}-{{.Second}}{{if .Sequence}}_{{.Sequence}}{{end}}", + split_filesize: 0, + split_duration: 0, + interval: 1, + }, + + // Watch for changes in LogLevel + watchLogLevel() { + this.$watch("settings.log_level", async (newVal, oldVal) => { + if (newVal !== oldVal) { + await this.updateLogLevel(); + } + }); + }, + + // Compute the channels to display for the current page + get paginatedChannels() { + const start = (this.currentPage - 1) * this.itemsPerPage; + return this.channels.slice(start, start + this.itemsPerPage); + }, + + // Calculate total pages + get totalPages() { + return Math.ceil(this.channels.length / this.itemsPerPage); + }, + + // Change page on click + goToPage(page) { + if (page >= 1 && page <= this.totalPages) { + this.currentPage = page; + } + }, + // openCreateDialog + openCreateDialog() { + document.getElementById("create-dialog").showModal(); + }, + + // closeCreateDialog + closeCreateDialog() { + document.getElementById("create-dialog").close(); + this.resetCreateDialog(); + }, + + // submitCreateDialog + submitCreateDialog() { + this.createChannel(); + this.closeCreateDialog(); + }, + + // error + error() { + alert("Error occurred, please refresh the page if something is wrong."); + }, + + // + async call(path, body) { + try { + var resp = await fetch(`/api/${path}`, { + body: JSON.stringify(body), + method: "POST", + }); + if (resp.status !== 200) { + this.error(); + return [null, true]; + } + return [await resp.json(), false]; + } catch { + this.error(); + return [null, true]; + } + }, + + // getSettings + async getSettings() { + var [resp, err] = await this.call("get_settings", {}); + if (!err) { + this.settings = resp; + this.resetCreateDialog(); + await this.updateLogLevel(); + } + }, + + // init + async init() { + document + .getElementById('create-dialog') + .addEventListener('close', () => this.resetCreateDialog()); + + await this.getSettings(); // Ensure settings are loaded + this.watchLogLevel(); // Start watching LogLevel after settings load + await this.listChannels(); + this.listenUpdate(); + }, + + async updateLogLevel() { + const [_, err] = await this.call('update_log_level', { + log_level: this.settings.log_level, + }); + + if (err) { + this.error(); + } + }, + + // resetCreateDialog + resetCreateDialog() { + document.getElementById("splitting-accordion").open = false; + + // Ensure settings are loaded before resetting form_data + this.form_data = { + username: "", + resolution: this.settings.resolution?.toString() || "1080", + resolution_fallback: this.settings.resolution_fallback || "down", + framerate: this.settings.framerate?.toString() || "30", + filename_pattern: this.settings.filename_pattern || "{{.Username}}/{{.Year}}-{{.Month}}-{{.Day}}/{{.Username}}_{{.Hour}}-{{.Minute}}-{{.Second}}{{if .Sequence}}_{{.Sequence}}{{end}}", + split_filesize: this.settings.split_filesize?.toString() || "0", + split_duration: this.settings.split_duration?.toString() || "30", + interval: this.settings.interval?.toString() || "1", + }; + }, + + // createChannel + async createChannel() { + await this.call("create_channel", { + username: this.form_data.username, + resolution: parseInt(this.form_data.resolution), + resolution_fallback: this.form_data.resolution_fallback, + framerate: parseInt(this.form_data.framerate), + filename_pattern: this.form_data.filename_pattern, + split_filesize: parseInt(this.form_data.split_filesize), + split_duration: parseInt(this.form_data.split_duration), + interval: parseInt(this.form_data.interval), + }); + }, + + // deleteChannel + async deleteChannel(username) { + if (!confirm(`Are you sure you want to delete the channel "${username}"?`)) { + return; + } + var [_, err] = await this.call("delete_channel", { username }); + if (!err) { + this.channels = this.channels.filter((ch) => ch.username !== username); + } + }, + + // pauseChannel + async pauseChannel(username) { + await this.call("pause_channel", { username }); + }, + + // terminateProgram + async terminateProgram() { + if (confirm("Are you sure you want to terminate the program?")) { + alert("The program is terminated, any error messages are safe to ignore."); + await this.call("terminate_program", {}); + } + }, + + // resumeChannel + async resumeChannel(username) { + await this.call("resume_channel", { username }); + }, + + // listChannels + async listChannels() { + if (this.is_updating_channels) { + return; + } + var [resp, err] = await this.call("list_channels", {}); + if (!err) { + this.channels = resp.channels; + this.currentPage = 1; + this.channels.forEach((ch) => { + this.scrollLogs(ch.username); + }); + } + this.is_updating_channels = false; + }, + + // listenUpdate + listenUpdate() { + var source = new EventSource("/api/listen_update"); + + source.onmessage = (event) => { + var data = JSON.parse(event.data); + + // If the channel is not in the list or is stopped, refresh the list. + if (!this.channels.some((ch) => ch.username === data.username) || data.is_stopped) { + this.listChannels(); + return; + } + + var index = this.channels.findIndex((ch) => ch.username === data.username); + + if (index === -1) { + return; + } + + this.channels[index].segment_duration = data.segment_duration; + this.channels[index].segment_filesize = data.segment_filesize; + this.channels[index].filename = data.filename; + this.channels[index].last_streamed_at = data.last_streamed_at; + this.channels[index].is_online = data.is_online; + this.channels[index].is_paused = data.is_paused; + this.channels[index].logs = [...this.channels[index].logs, data.log]; + + if (this.channels[index].logs.length > 100) { + this.channels[index].logs = this.channels[index].logs.slice(-100); + } + + this.scrollLogs(data.username); + }; + + source.onerror = (err) => { + source.close(); + }; + }, + + downloadLogs(username) { + var a = window.document.createElement("a"); + a.href = window.URL.createObjectURL(new Blob([this.channels[this.channels.findIndex((ch) => ch.username === username)].logs.join("\n")], { type: "text/plain", oneTimeOnly: true })); + a.download = `${username}_logs.txt`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + }, + + // + scrollLogs(username) { + // Wait for the DOM to update. + setTimeout(() => { + var logs_element = document.getElementById(`${username}-logs`); + + if (!logs_element) { + return; + } + logs_element.scrollTop = logs_element.scrollHeight; + }, 1); + }, + }; } diff --git a/main.go b/main.go index 6ce22e4..9abbb81 100644 --- a/main.go +++ b/main.go @@ -31,7 +31,7 @@ const logo = ` func main() { app := &cli.App{ Name: "chaturbate-dvr", - Version: "1.0.6", + Version: "1.0.7", Usage: "Records your favorite Chaturbate stream 😎🫵", Flags: []cli.Flag{ &cli.StringFlag{ @@ -90,7 +90,7 @@ func main() { }, &cli.StringFlag{ Name: "log-level", - Usage: "log level, availables: 'DEBUG', 'INFO', 'WARN', 'ERROR'", + Usage: "log level, available: 'DEBUG', 'INFO', 'WARN', 'ERROR'", Value: "INFO", }, &cli.StringFlag{ @@ -163,6 +163,10 @@ func startWeb(c *cli.Context) error { guiUsername := c.String("gui-username") guiPassword := c.String("gui-password") + logLevel := c.String("log-level") + + chaturbate.InitGlobalLogLevel(chaturbate.LogType(logLevel)) + var authorized = r.Group("/") var authorizedApi = r.Group("/api") @@ -186,6 +190,7 @@ func startWeb(c *cli.Context) error { authorizedApi.GET("/listen_update", handler.NewListenUpdateHandler(m, c).Handle) authorizedApi.POST("/get_settings", handler.NewGetSettingsHandler(c).Handle) authorizedApi.POST("/terminate_program", handler.NewTerminateProgramHandler(c).Handle) + authorizedApi.POST("/update_log_level", handler.NewUpdateLogLevelHandler(c).Handle) fmt.Printf("👋 Visit http://localhost:%s to use the Web UI\n", c.String("port"))