chaturbate-dvr/channel/channel_file.go

131 lines
3.4 KiB
Go

package channel
import (
"bytes"
"errors"
"fmt"
"html/template"
"os"
"path/filepath"
"time"
)
// Pattern holds the date/time and sequence information for the filename pattern
type Pattern struct {
Username string
Year string
Month string
Day string
Hour string
Minute string
Second string
Sequence int
}
// NextFile prepares the next file to be created, by cleaning up the last file and generating a new one
func (ch *Channel) NextFile() error {
if err := ch.Cleanup(); err != nil {
return err
}
filename, err := ch.GenerateFilename()
if err != nil {
return err
}
if err := ch.CreateNewFile(filename); err != nil {
return err
}
// Increment the sequence number for the next file
ch.Sequence++
return nil
}
// Cleanup cleans the file and resets it, called when the stream errors out or before next file was created.
func (ch *Channel) Cleanup() error {
if ch.File == nil {
return nil
}
filename := ch.File.Name()
defer func() {
ch.Filesize = 0
ch.Duration = 0
}()
// Sync the file to ensure data is written to disk
if err := ch.File.Sync(); err != nil && !errors.Is(err, os.ErrClosed) {
return fmt.Errorf("sync file: %w", err)
}
if err := ch.File.Close(); err != nil && !errors.Is(err, os.ErrClosed) {
return fmt.Errorf("close file: %w", err)
}
// Delete the empty file
fileInfo, err := os.Stat(filename)
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("stat file delete zero file: %w", err)
}
if fileInfo != nil && fileInfo.Size() == 0 {
if err := os.Remove(filename); err != nil {
return fmt.Errorf("remove zero file: %w", err)
}
}
return nil
}
// GenerateFilename creates a filename based on the configured pattern and the current timestamp
func (ch *Channel) GenerateFilename() (string, error) {
var buf bytes.Buffer
// Parse the filename pattern defined in the channel's config
tpl, err := template.New("filename").Parse(ch.Config.Pattern)
if err != nil {
return "", fmt.Errorf("filename pattern error: %w", err)
}
// Get the current time based on the Unix timestamp when the stream was started
t := time.Unix(ch.StreamedAt, 0)
pattern := &Pattern{
Username: ch.Config.Username,
Sequence: ch.Sequence,
Year: t.Format("2006"),
Month: t.Format("01"),
Day: t.Format("02"),
Hour: t.Format("15"),
Minute: t.Format("04"),
Second: t.Format("05"),
}
if err := tpl.Execute(&buf, pattern); err != nil {
return "", fmt.Errorf("template execution error: %w", err)
}
return buf.String(), nil
}
// CreateNewFile creates a new file for the channel using the given filename
func (ch *Channel) CreateNewFile(filename string) error {
// Ensure the directory exists before creating the file
if err := os.MkdirAll(filepath.Dir(filename), 0777); err != nil {
return fmt.Errorf("mkdir all: %w", err)
}
// Open the file in append mode, create it if it doesn't exist
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)
}
ch.File = file
return nil
}
// ShouldSwitchFile determines whether a new file should be created.
func (ch *Channel) ShouldSwitchFile() bool {
maxFilesizeBytes := ch.Config.MaxFilesize * 1024 * 1024
maxDurationSeconds := ch.Config.MaxDuration * 60
return (ch.Duration >= float64(maxDurationSeconds) && ch.Config.MaxDuration > 0) ||
(ch.Filesize >= maxFilesizeBytes && ch.Config.MaxFilesize > 0)
}