- Add HLS proxy (hlsproxy) for rewriting m3u8 playlists and proxying segments with correct Referer/Origin headers (uses ?domain= param) - Add playerconfig service for detecting stream types (VIPLeague, DaddyLive, HLS) and extracting auth params from ksohls pages - Add VIPLeague URL resolution: extract slug from URL path, match against DaddyLive 24/7 channel index with token-based scoring - Replace Clappr with direct HLS.js player for better compatibility - Add CryptoJS CDN for DaddyLive auth module support - Disable CrowdSec on f1-stream ingress to prevent false positives - Bump image to v1.3.1
163 lines
4.2 KiB
Go
163 lines
4.2 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"f1-stream/internal/auth"
|
|
"f1-stream/internal/extractor"
|
|
"f1-stream/internal/healthcheck"
|
|
"f1-stream/internal/models"
|
|
"f1-stream/internal/playerconfig"
|
|
"f1-stream/internal/scraper"
|
|
"f1-stream/internal/server"
|
|
"f1-stream/internal/store"
|
|
)
|
|
|
|
func main() {
|
|
listenAddr := envOr("LISTEN_ADDR", ":8080")
|
|
dataDir := envOr("DATA_DIR", "/data")
|
|
scrapeInterval := envDuration("SCRAPE_INTERVAL", 15*time.Minute)
|
|
validateTimeout := envDuration("SCRAPER_VALIDATE_TIMEOUT", 10*time.Second)
|
|
adminUsername := os.Getenv("ADMIN_USERNAME")
|
|
sessionTTL := envDuration("SESSION_TTL", 720*time.Hour)
|
|
headlessEnabled := os.Getenv("HEADLESS_EXTRACT_ENABLED") == "true"
|
|
rpID := envOr("WEBAUTHN_RPID", "localhost")
|
|
rpOrigin := envOr("WEBAUTHN_ORIGIN", "http://localhost:8080")
|
|
rpDisplayName := envOr("WEBAUTHN_DISPLAY_NAME", "F1 Stream")
|
|
|
|
// Initialize store
|
|
st, err := store.New(dataDir)
|
|
if err != nil {
|
|
log.Fatalf("failed to init store: %v", err)
|
|
}
|
|
|
|
// Seed default streams
|
|
if err := st.SeedStreams(defaultStreams()); err != nil {
|
|
log.Printf("warning: failed to seed streams: %v", err)
|
|
}
|
|
|
|
// Initialize auth
|
|
origins := strings.Split(rpOrigin, ",")
|
|
a, err := auth.New(st, rpDisplayName, rpID, origins, adminUsername, sessionTTL)
|
|
if err != nil {
|
|
log.Fatalf("failed to init auth: %v", err)
|
|
}
|
|
|
|
// Initialize scraper
|
|
sc := scraper.New(st, scrapeInterval, validateTimeout)
|
|
|
|
// Initialize health checker
|
|
healthInterval := envDuration("HEALTH_CHECK_INTERVAL", 5*time.Minute)
|
|
healthTimeout := envDuration("HEALTH_CHECK_TIMEOUT", 10*time.Second)
|
|
hc := healthcheck.New(st, healthInterval, healthTimeout)
|
|
|
|
// Initialize server
|
|
pc := playerconfig.New()
|
|
srv := server.New(st, a, sc, pc, origins, headlessEnabled)
|
|
|
|
// Start scraper in background
|
|
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
|
defer cancel()
|
|
|
|
// Initialize headless browser if enabled
|
|
if headlessEnabled {
|
|
extractor.Init()
|
|
defer extractor.Stop()
|
|
// Configure TURN server if provided
|
|
if turnURL := os.Getenv("TURN_URL"); turnURL != "" {
|
|
turnSecret := os.Getenv("TURN_SHARED_SECRET")
|
|
turnInternalURL := os.Getenv("TURN_INTERNAL_URL")
|
|
extractor.SetTURNConfig(turnURL, turnSecret, turnInternalURL)
|
|
}
|
|
log.Println("headless video extraction enabled")
|
|
}
|
|
|
|
go sc.Run(ctx)
|
|
go hc.Run(ctx)
|
|
|
|
// Clean expired sessions periodically
|
|
go func() {
|
|
sessionTicker := time.NewTicker(1 * time.Hour)
|
|
defer sessionTicker.Stop()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-sessionTicker.C:
|
|
st.CleanExpiredSessions()
|
|
}
|
|
}
|
|
}()
|
|
|
|
httpSrv := &http.Server{
|
|
Addr: listenAddr,
|
|
Handler: srv.Handler(),
|
|
}
|
|
|
|
go func() {
|
|
<-ctx.Done()
|
|
log.Println("shutting down server...")
|
|
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer shutdownCancel()
|
|
httpSrv.Shutdown(shutdownCtx)
|
|
}()
|
|
|
|
log.Printf("starting server on %s", listenAddr)
|
|
if err := httpSrv.ListenAndServe(); err != http.ErrServerClosed {
|
|
log.Fatalf("server error: %v", err)
|
|
}
|
|
log.Println("server stopped")
|
|
}
|
|
|
|
func defaultStreams() []models.Stream {
|
|
now := time.Now()
|
|
streams := []struct {
|
|
url, title string
|
|
}{
|
|
{"https://wearechecking.live/streams-pages/motorsports", "WeAreChecking - Motorsports"},
|
|
{"https://vipleague.im/formula-1-schedule-streaming-links", "VIPLeague - F1"},
|
|
{"https://www.vipbox.lc/", "VIPBox"},
|
|
{"https://f1box.me/", "F1Box"},
|
|
{"https://1stream.vip/formula-1-streams/", "1Stream - F1"},
|
|
}
|
|
var result []models.Stream
|
|
for i, s := range streams {
|
|
result = append(result, models.Stream{
|
|
ID: fmt.Sprintf("default-%d", i),
|
|
URL: s.url,
|
|
Title: s.title,
|
|
SubmittedBy: "system",
|
|
Published: true,
|
|
Source: "system",
|
|
CreatedAt: now,
|
|
})
|
|
}
|
|
return result
|
|
}
|
|
|
|
func envOr(key, fallback string) string {
|
|
if v := os.Getenv(key); v != "" {
|
|
return v
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
func envDuration(key string, fallback time.Duration) time.Duration {
|
|
if v := os.Getenv(key); v != "" {
|
|
d, err := time.ParseDuration(v)
|
|
if err != nil {
|
|
log.Printf("warning: invalid %s=%q, using default %v", key, v, fallback)
|
|
return fallback
|
|
}
|
|
return d
|
|
}
|
|
return fallback
|
|
}
|