streamrecorder/main.go
2025-08-08 07:23:08 +02:00

309 lines
6.8 KiB
Go
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"path/filepath"
"os"
"os/exec"
"regexp"
"strconv"
"strings"
"time"
"github.com/grafov/m3u8"
)
var roomDossierRegexp = regexp.MustCompile(`window\.initialRoomDossier = "(.*?)"`)
// --- main ---
func main() {
if len(os.Args) < 2 {
fmt.Println("Verwendung: recorder.exe <Chaturbate-URL oder Benutzername> [-o <Pfad/zieldatei.ts>]")
os.Exit(1)
}
arg := os.Args[1]
username := extractUsername(arg)
outputPath := fmt.Sprintf("%s_%s.ts", username, time.Now().Format("20060102_150405"))
for i := 2; i < len(os.Args); i++ {
if os.Args[i] == "-o" && i+1 < len(os.Args) {
outputPath = os.Args[i+1]
break
}
}
ctx := context.Background()
body, err := fetchPage("https://chaturbate.com/" + username)
if err != nil {
fmt.Println("❌ Fehler beim Laden der Seite:", err)
os.Exit(1)
}
hlsURL, err := ParseStream(body)
if err != nil {
fmt.Println("❌ Stream nicht gefunden oder offline:", err)
os.Exit(1)
}
playlist, err := FetchPlaylist(ctx, hlsURL)
if err != nil {
fmt.Println("❌ Fehler beim Abrufen der Playlist:", err)
os.Exit(1)
}
file, err := os.Create(outputPath)
if err != nil {
fmt.Println("❌ Datei konnte nicht erstellt werden:", err)
os.Exit(1)
}
defer file.Close()
fmt.Println("📡 Aufnahme gestartet:", outputPath)
err = playlist.WatchSegments(ctx, func(b []byte, _ float64) error {
_, err := file.Write(b)
if err != nil {
fmt.Println("❌ Fehler beim Schreiben in Datei:", err)
}
return err
})
if err != nil {
fmt.Println("❌ Aufnahmefehler:", err)
}
mp4Out := outputPath
ext := filepath.Ext(outputPath)
// Falls nicht bereits .mp4 als Endung existiert
if ext != ".mp4" {
mp4Out = strings.TrimSuffix(outputPath, ext) + ".mp4"
}
if err := exec.Command("ffmpeg", "-y", "-i", outputPath, "-c", "copy", mp4Out).Run(); err != nil {
fmt.Println("⚠️ Fehler bei Umwandlung in MP4:", err)
} else {
fmt.Println("✅ Umwandlung abgeschlossen:", mp4Out)
}
}
// --- helper ---
func extractUsername(input string) string {
input = strings.TrimPrefix(input, "https://")
input = strings.TrimPrefix(input, "http://")
input = strings.TrimPrefix(input, "www.")
if strings.HasPrefix(input, "chaturbate.com/") {
return strings.TrimPrefix(input, "chaturbate.com/")
}
return strings.TrimSpace(input)
}
func fetchPage(url string) (string, error) {
client := http.Client{Timeout: 10 * time.Second}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return "", err
}
req.Header.Set("User-Agent", "Mozilla/5.0")
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return "", fmt.Errorf("HTTP %d: %s", resp.StatusCode, url)
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(data), nil
}
func ParseStream(html string) (string, error) {
matches := roomDossierRegexp.FindStringSubmatch(html)
if len(matches) < 2 {
return "", errors.New("room dossier nicht gefunden")
}
encoded := matches[1]
decoded, err := strconv.Unquote(`"` + strings.ReplaceAll(encoded, `\\u`, `\u`) + `"`)
if err != nil {
return "", fmt.Errorf("Unicode-decode failed: %w", err)
}
var rd struct {
HLSSource string `json:"hls_source"`
}
if err := json.Unmarshal([]byte(decoded), &rd); err != nil {
return "", fmt.Errorf("JSON-parse failed: %w", err)
}
if rd.HLSSource == "" {
return "", errors.New("kein HLS-Quell-URL im JSON")
}
return rd.HLSSource, nil
}
type Playlist struct {
PlaylistURL string
RootURL string
Resolution int
Framerate int
}
type Resolution struct {
Framerate map[int]string
Width int
}
func FetchPlaylist(ctx context.Context, hlsSource string) (*Playlist, error) {
if hlsSource == "" {
return nil, errors.New("HLS-URL leer")
}
resp, err := http.Get(hlsSource)
if err != nil {
return nil, fmt.Errorf("Fehler beim Laden der Playlist: %w", err)
}
defer resp.Body.Close()
playlist, listType, err := m3u8.DecodeFrom(resp.Body, true)
if err != nil || listType != m3u8.MASTER {
return nil, errors.New("keine gültige Master-Playlist")
}
master := playlist.(*m3u8.MasterPlaylist)
var bestURI string
var bestWidth int
var bestFramerate int
for _, variant := range master.Variants {
if variant == nil || variant.Resolution == "" {
continue
}
parts := strings.Split(variant.Resolution, "x")
if len(parts) != 2 {
continue
}
width, err := strconv.Atoi(parts[1])
if err != nil {
continue
}
fr := 30
if strings.Contains(variant.Name, "FPS:60.0") {
fr = 60
}
// Besser, wenn größere Auflösung oder gleiche Auflösung mit höherem Framerate
if width > bestWidth || (width == bestWidth && fr > bestFramerate) {
bestWidth = width
bestFramerate = fr
bestURI = variant.URI
}
}
if bestURI == "" {
return nil, errors.New("keine gültige Auflösung gefunden")
}
root := hlsSource[:strings.LastIndex(hlsSource, "/")+1]
return &Playlist{
PlaylistURL: root + bestURI,
RootURL: root,
Resolution: bestWidth,
Framerate: bestFramerate,
}, nil
}
func (p *Playlist) WatchSegments(ctx context.Context, handler func([]byte, float64) error) error {
var lastSeq int64 = -1
client := http.Client{Timeout: 10 * time.Second}
emptyRounds := 0
const maxEmptyRounds = 5 // z.B. nach 5 leeren Runden abbrechen
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
resp, err := client.Get(p.PlaylistURL)
if err != nil {
emptyRounds++
if emptyRounds >= maxEmptyRounds {
return errors.New("❌ Playlist nicht mehr erreichbar Stream vermutlich offline")
}
time.Sleep(2 * time.Second)
continue
}
playlist, listType, err := m3u8.DecodeFrom(resp.Body, true)
resp.Body.Close()
if err != nil || listType != m3u8.MEDIA {
emptyRounds++
if emptyRounds >= maxEmptyRounds {
return errors.New("❌ Fehlerhafte Playlist möglicherweise offline")
}
time.Sleep(2 * time.Second)
continue
}
media := playlist.(*m3u8.MediaPlaylist)
newSegment := false
for _, segment := range media.Segments {
if segment == nil {
continue
}
if int64(segment.SeqId) <= lastSeq {
continue
}
lastSeq = int64(segment.SeqId)
newSegment = true
segmentURL := p.RootURL + segment.URI
segResp, err := client.Get(segmentURL)
if err != nil {
continue
}
data, err := io.ReadAll(segResp.Body)
segResp.Body.Close()
if err != nil || len(data) == 0 {
continue
}
if err := handler(data, segment.Duration); err != nil {
return err
}
}
if newSegment {
emptyRounds = 0
} else {
emptyRounds++
if emptyRounds >= maxEmptyRounds {
return errors.New("🛑 Keine neuen HLS-Segmente empfangen Stream vermutlich beendet oder offline.")
}
}
time.Sleep(1 * time.Second)
}
}