mirror of
https://github.com/teacat/chaturbate-dvr.git
synced 2025-10-29 16:59:59 +00:00
WIP
This commit is contained in:
@@ -63,10 +63,10 @@ func (h *GetChannelHandler) Handle(c *gin.Context) {
|
||||
ChannelURL: channel.ChannelURL,
|
||||
Filename: channel.Filename(),
|
||||
LastStreamedAt: channel.LastStreamedAt,
|
||||
SegmentDuration: DurationStr(channel.SegmentDuration),
|
||||
SplitDuration: DurationStr(channel.SplitDuration),
|
||||
SegmentFilesize: ByteStr(channel.SegmentFilesize),
|
||||
SplitFilesize: MBStr(channel.SplitFilesize),
|
||||
SegmentDuration: channel.SegmentDurationStr(),
|
||||
SplitDuration: channel.SplitDurationStr(),
|
||||
SegmentFilesize: channel.SegmentFilesizeStr(),
|
||||
SplitFilesize: channel.SplitFilesizeStr(),
|
||||
IsOnline: channel.IsOnline,
|
||||
IsPaused: channel.IsPaused,
|
||||
Logs: channel.Logs,
|
||||
|
||||
@@ -15,6 +15,7 @@ type GetSettingsHandlerRequest struct {
|
||||
}
|
||||
|
||||
type GetSettingsHandlerResponse struct {
|
||||
Version string `json:"version"`
|
||||
Framerate int `json:"framerate"`
|
||||
Resolution int `json:"resolution"`
|
||||
ResolutionFallback string `json:"resolution_fallback"`
|
||||
@@ -49,6 +50,7 @@ func (h *GetSettingsHandler) Handle(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, &GetSettingsHandlerResponse{
|
||||
Version: h.cli.App.Version,
|
||||
Framerate: h.cli.Int("framerate"),
|
||||
Resolution: h.cli.Int("resolution"),
|
||||
ResolutionFallback: h.cli.String("resolution-fallback"),
|
||||
|
||||
@@ -70,10 +70,10 @@ func (h *ListChannelsHandler) Handle(c *gin.Context) {
|
||||
ChannelURL: channel.ChannelURL,
|
||||
Filename: channel.Filename(),
|
||||
LastStreamedAt: channel.LastStreamedAt,
|
||||
SegmentDuration: DurationStr(channel.SegmentDuration),
|
||||
SplitDuration: DurationStr(channel.SplitDuration),
|
||||
SegmentFilesize: ByteStr(channel.SegmentFilesize),
|
||||
SplitFilesize: MBStr(channel.SplitFilesize),
|
||||
SegmentDuration: channel.SegmentDurationStr(),
|
||||
SplitDuration: channel.SplitDurationStr(),
|
||||
SegmentFilesize: channel.SegmentFilesizeStr(),
|
||||
SplitFilesize: channel.SplitFilesizeStr(),
|
||||
IsOnline: channel.IsOnline,
|
||||
IsPaused: channel.IsPaused,
|
||||
Logs: channel.Logs,
|
||||
|
||||
@@ -46,8 +46,9 @@ func (h *ListenUpdateHandler) Handle(c *gin.Context) {
|
||||
"is_paused": update.IsPaused,
|
||||
"is_online": update.IsOnline,
|
||||
"last_streamed_at": update.LastStreamedAt,
|
||||
"segment_duration": DurationStr(update.SegmentDuration),
|
||||
"segment_filesize": ByteStr(update.SegmentFilesize),
|
||||
"segment_duration": update.SegmentDurationStr(),
|
||||
"segment_filesize": update.SegmentFilesizeStr(),
|
||||
"filename": update.Filename,
|
||||
})
|
||||
return true
|
||||
})
|
||||
|
||||
46
handler/terminate_program.go
Normal file
46
handler/terminate_program.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
//=======================================================
|
||||
// Request & Response
|
||||
//=======================================================
|
||||
|
||||
type TerminateProgramRequest struct {
|
||||
}
|
||||
|
||||
type TerminateProgramResponse struct {
|
||||
}
|
||||
|
||||
//=======================================================
|
||||
// Factory
|
||||
//=======================================================
|
||||
|
||||
type TerminateProgramHandler struct {
|
||||
cli *cli.Context
|
||||
}
|
||||
|
||||
func NewTerminateProgramHandler(cli *cli.Context) *TerminateProgramHandler {
|
||||
return &TerminateProgramHandler{cli}
|
||||
}
|
||||
|
||||
//=======================================================
|
||||
// Handle
|
||||
//=======================================================
|
||||
|
||||
func (h *TerminateProgramHandler) Handle(c *gin.Context) {
|
||||
var req *TerminateProgramRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.AbortWithError(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
log.Println("program terminated by user request, see ya 👋")
|
||||
os.Exit(0)
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package handler
|
||||
|
||||
import "fmt"
|
||||
|
||||
func DurationStr(seconds int) string {
|
||||
hours := seconds / 3600
|
||||
seconds %= 3600
|
||||
minutes := seconds / 60
|
||||
seconds %= 60
|
||||
return fmt.Sprintf("%02d:%02d:%02d", hours, minutes, seconds)
|
||||
}
|
||||
|
||||
func ByteStr(bytes int) string {
|
||||
return fmt.Sprintf("%.2f MiB", float64(bytes)/1024/1024)
|
||||
}
|
||||
|
||||
func KBStr(kibs int) string {
|
||||
return fmt.Sprintf("%.2f MiB", float64(kibs)/1024)
|
||||
}
|
||||
|
||||
func MBStr(mibs int) string {
|
||||
return fmt.Sprintf("%.2f MiB", float64(mibs))
|
||||
}
|
||||
@@ -3,9 +3,9 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="stylesheet" href="./tocas/tocas.min.css" />
|
||||
<script src="./tocas/tocas.min.js"></script>
|
||||
<script src="./script.js"></script>
|
||||
<link rel="stylesheet" href="/static/tocas/tocas.min.css" />
|
||||
<script src="/static/tocas/tocas.min.js"></script>
|
||||
<script src="/static/script.js"></script>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@400;500;700&display=swap" rel="stylesheet" />
|
||||
@@ -16,6 +16,7 @@
|
||||
<!-- Create Dialog -->
|
||||
<dialog id="create-dialog" class="ts-modal is-large" data-clickaway="close">
|
||||
<div class="content">
|
||||
<!-- Header -->
|
||||
<div class="ts-content is-horizontally-padded is-secondary">
|
||||
<div class="ts-grid">
|
||||
<div class="column is-fluid">
|
||||
@@ -26,8 +27,13 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- / Header -->
|
||||
|
||||
<div class="ts-divider"></div>
|
||||
|
||||
<!-- Form -->
|
||||
<div class="ts-content is-vertically-padded">
|
||||
<!-- Field: Channel Username -->
|
||||
<div class="ts-control is-wide">
|
||||
<div class="label">Channel Username</div>
|
||||
<div class="content">
|
||||
@@ -37,7 +43,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- / Field: Channel Username -->
|
||||
|
||||
<!-- Field: Resolution -->
|
||||
<div class="ts-control is-wide has-top-spaced-large">
|
||||
<div class="label">Resolution</div>
|
||||
<div class="content">
|
||||
@@ -76,6 +84,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- / Field: Resolution -->
|
||||
|
||||
<!-- Field: Framerate -->
|
||||
<div class="ts-control is-wide has-top-spaced-large">
|
||||
<div class="label">Framerate</div>
|
||||
<div class="content">
|
||||
@@ -94,6 +105,9 @@
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<!-- / Field: Framerate -->
|
||||
|
||||
<!-- Field: Filename Pattern -->
|
||||
<div class="ts-control is-wide has-top-spaced-large">
|
||||
<div class="label">Filename Pattern</div>
|
||||
<div class="content">
|
||||
@@ -105,9 +119,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- / Field: Filename Pattern -->
|
||||
|
||||
<div class="ts-divider has-vertically-spaced-large"></div>
|
||||
|
||||
<!-- Field: Splitting Options -->
|
||||
<div class="ts-control is-wide has-top-spaced">
|
||||
<div class="label"></div>
|
||||
<div class="content">
|
||||
@@ -135,28 +151,35 @@
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
<!-- / Field: Splitting Options -->
|
||||
</div>
|
||||
<!-- / Form -->
|
||||
|
||||
<div class="ts-divider"></div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="ts-content is-secondary is-horizontally-padded">
|
||||
<div class="ts-wrap is-end-aligned">
|
||||
<button class="ts-button is-outlined is-secondary" x-on:click="closeCreateDialog">Cancel</button>
|
||||
<button class="ts-button is-primary" x-on:click="submitCreateDialog">Add Channel</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- / Footer -->
|
||||
</div>
|
||||
</dialog>
|
||||
<!-- / Create Dialog -->
|
||||
|
||||
<div class="ts-container is-narrow has-vertically-spaced-big">
|
||||
<!-- Main Section -->
|
||||
<div class="ts-container is-narrow has-vertically-padded-big">
|
||||
<!-- Header -->
|
||||
<div class="ts-grid is-bottom-aligned">
|
||||
<div class="column is-fluid">
|
||||
<div class="ts-header is-huge is-uppercased is-heavy has-leading-small">Chaturbate DVR</div>
|
||||
<div class="ts-text is-description is-bold">Version 1.0.0</div>
|
||||
<div class="ts-text is-description is-bold">Version <span x-text="settings.version"></span></div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="ts-wrap">
|
||||
<button class="ts-button is-outlined is-negative is-start-icon">
|
||||
<button class="ts-button is-outlined is-negative is-start-icon" x-on:click="terminateProgram()">
|
||||
<span class="ts-icon is-hand-icon"></span>
|
||||
Terminate
|
||||
</button>
|
||||
@@ -173,10 +196,9 @@
|
||||
<template x-if="channels.length === 0">
|
||||
<div>
|
||||
<div class="ts-divider has-vertically-spaced-large"></div>
|
||||
|
||||
<div class="ts-blankslate">
|
||||
<span class="ts-icon is-eye-low-vision-icon"></span>
|
||||
<div class="header">No channel was recording.</div>
|
||||
<div class="header">No channel was recording</div>
|
||||
<div class="description">Add a new Chaturbate channel to start the recording.</div>
|
||||
<div class="action">
|
||||
<button class="ts-button is-start-icon" x-on:click="openCreateDialog">
|
||||
@@ -198,10 +220,12 @@
|
||||
<!-- / Divider -->
|
||||
|
||||
<div class="ts-wrap is-vertical is-relaxed">
|
||||
<!-- Each Channel -->
|
||||
<!-- Channel -->
|
||||
<template x-for="channel in channels" :key="channel.username">
|
||||
<div class="ts-box is-horizontal">
|
||||
<!-- Left Section -->
|
||||
<div class="ts-content is-padded" style="flex: 1.8; display: flex; flex-direction: column">
|
||||
<!-- Header -->
|
||||
<div class="ts-grid is-middle-aligned">
|
||||
<div class="column is-fluid">
|
||||
<div class="ts-header">
|
||||
@@ -221,13 +245,23 @@
|
||||
<button class="ts-button is-secondary is-short is-outlined is-dense" x-on:click="downloadLogs(channel.username)">Download Logs</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- / Header -->
|
||||
|
||||
<!-- Logs -->
|
||||
<div class="ts-input has-top-spaced" style="flex: 1">
|
||||
<textarea class="has-full-height" x-bind:id="`${channel.username}-logs`" x-text="channel.logs.join('\n')" readonly></textarea>
|
||||
</div>
|
||||
<!-- / Logs -->
|
||||
</div>
|
||||
<!-- / Left Section -->
|
||||
|
||||
<div class="ts-divider is-vertical"></div>
|
||||
|
||||
<!-- Right Section -->
|
||||
<div class="ts-content is-padded has-break-all" style="flex: 1; min-width: 300px">
|
||||
<div class="ts-text is-description is-uppercased">Information</div>
|
||||
|
||||
<!-- Info: Channel URL -->
|
||||
<div class="ts-grid has-top-spaced-large">
|
||||
<div class="column has-leading-none" style="width: 16px">
|
||||
<span class="ts-icon is-link-icon"></span>
|
||||
@@ -237,12 +271,15 @@
|
||||
<a class="ts-text is-link is-external-link" x-bind:href="channel.channel_url" x-text="channel.channel_url" target="_blank"></a>
|
||||
</div>
|
||||
</div>
|
||||
<!-- / Info: Channel URL -->
|
||||
|
||||
<!-- Info: Filename -->
|
||||
<div class="ts-grid has-top-spaced">
|
||||
<div class="column has-leading-none" style="width: 16px">
|
||||
<span class="ts-icon is-folder-icon"></span>
|
||||
</div>
|
||||
<div class="column is-fluid has-leading-small">
|
||||
<div class="ts-text is-label">Saved to</div>
|
||||
<div class="ts-text is-label">Filename</div>
|
||||
|
||||
<template x-if="channel.filename">
|
||||
<code class="ts-text is-code" x-text="channel.filename"></code>
|
||||
@@ -252,6 +289,9 @@
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<!-- / Info: Filename -->
|
||||
|
||||
<!-- Info: Last streamed at -->
|
||||
<div class="ts-grid has-top-spaced">
|
||||
<div class="column has-leading-none" style="width: 16px">
|
||||
<span class="ts-icon is-tower-broadcast-icon"></span>
|
||||
@@ -266,6 +306,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- / Info: Last streamed at -->
|
||||
|
||||
<!-- Info: Segment duration -->
|
||||
<div class="ts-grid has-top-spaced">
|
||||
<div class="column has-leading-none" style="width: 16px">
|
||||
<span class="ts-icon is-clock-icon"></span>
|
||||
@@ -280,6 +323,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- / Info: Segment duration -->
|
||||
|
||||
<!-- Info: Segment filesize -->
|
||||
<div class="ts-grid has-top-spaced">
|
||||
<div class="column has-leading-none" style="width: 16px">
|
||||
<span class="ts-icon is-chart-pie-icon"></span>
|
||||
@@ -294,11 +340,17 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- / Info: Segment filesize -->
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="ts-grid is-2-columns has-top-spaced-large">
|
||||
<div class="column">
|
||||
<template x-if="!channel.is_paused">
|
||||
<button class="ts-button is-start-icon is-secondary is-fluid" x-on:click="pauseChannel(channel.username)">
|
||||
<button
|
||||
class="ts-button is-start-icon is-secondary is-fluid"
|
||||
x-bind:disabled="!channel.is_online"
|
||||
x-on:click="pauseChannel(channel.username)"
|
||||
>
|
||||
<span class="ts-icon is-pause-icon"></span>
|
||||
Pause
|
||||
</button>
|
||||
@@ -312,7 +364,7 @@
|
||||
</div>
|
||||
<div class="column">
|
||||
<button
|
||||
class="ts-button is-start-icon is-secondary is-fluid"
|
||||
class="ts-button is-start-icon is-secondary is-negative is-fluid"
|
||||
data-tooltip="Stop and remove the channel from the list."
|
||||
x-on:click="deleteChannel(channel.username)"
|
||||
>
|
||||
@@ -321,11 +373,14 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- / Actions -->
|
||||
</div>
|
||||
<!-- / Right Section -->
|
||||
</div>
|
||||
<!-- / Each Channel -->
|
||||
</template>
|
||||
<!-- / Channel -->
|
||||
</div>
|
||||
</div>
|
||||
<!-- / Main Section -->
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -38,7 +38,7 @@ function data() {
|
||||
//
|
||||
async call(path, body) {
|
||||
try {
|
||||
var resp = await fetch(`http://localhost:8080/api/${path}`, {
|
||||
var resp = await fetch(`/api/${path}`, {
|
||||
body: JSON.stringify(body),
|
||||
method: "POST",
|
||||
})
|
||||
@@ -101,6 +101,9 @@ function data() {
|
||||
|
||||
// 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)
|
||||
@@ -112,6 +115,15 @@ function data() {
|
||||
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 })
|
||||
@@ -134,7 +146,7 @@ function data() {
|
||||
|
||||
// listenUpdate
|
||||
listenUpdate() {
|
||||
var source = new EventSource("http://localhost:8080/api/listen_update")
|
||||
var source = new EventSource("/api/listen_update")
|
||||
|
||||
source.onmessage = event => {
|
||||
var data = JSON.parse(event.data)
|
||||
@@ -174,13 +186,9 @@ function data() {
|
||||
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 = 'test.txt';
|
||||
|
||||
// Append anchor to body.
|
||||
a.download = `${username}_logs.txt`
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
|
||||
// Remove anchor from body
|
||||
document.body.removeChild(a);
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user