Debug functiional
This commit is contained in:
136
internal/app.go
136
internal/app.go
@@ -14,12 +14,6 @@ import (
|
|||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
)
|
)
|
||||||
|
|
||||||
//
|
|
||||||
// ────────────────────────────────
|
|
||||||
// KEY MAP + HELP
|
|
||||||
// ────────────────────────────────
|
|
||||||
//
|
|
||||||
|
|
||||||
type keyMap struct {
|
type keyMap struct {
|
||||||
Up, Down, Left, Right key.Binding
|
Up, Down, Left, Right key.Binding
|
||||||
Enter, Quit, Refresh key.Binding
|
Enter, Quit, Refresh key.Binding
|
||||||
@@ -53,12 +47,6 @@ func (k keyMap) FullHelp() [][]key.Binding {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
|
||||||
// ────────────────────────────────
|
|
||||||
// MESSAGE TYPES
|
|
||||||
// ────────────────────────────────
|
|
||||||
//
|
|
||||||
|
|
||||||
type (
|
type (
|
||||||
sportsLoadedMsg []Sport
|
sportsLoadedMsg []Sport
|
||||||
matchesLoadedMsg struct {
|
matchesLoadedMsg struct {
|
||||||
@@ -68,14 +56,9 @@ type (
|
|||||||
streamsLoadedMsg []Stream
|
streamsLoadedMsg []Stream
|
||||||
errorMsg error
|
errorMsg error
|
||||||
launchStreamMsg struct{ URL string }
|
launchStreamMsg struct{ URL string }
|
||||||
|
debugLogMsg string
|
||||||
)
|
)
|
||||||
|
|
||||||
//
|
|
||||||
// ────────────────────────────────
|
|
||||||
// MODEL
|
|
||||||
// ────────────────────────────────
|
|
||||||
//
|
|
||||||
|
|
||||||
type focusCol int
|
type focusCol int
|
||||||
type viewMode int
|
type viewMode int
|
||||||
|
|
||||||
@@ -93,7 +76,6 @@ const (
|
|||||||
|
|
||||||
type Model struct {
|
type Model struct {
|
||||||
apiClient *Client
|
apiClient *Client
|
||||||
|
|
||||||
styles Styles
|
styles Styles
|
||||||
keys keyMap
|
keys keyMap
|
||||||
help help.Model
|
help help.Model
|
||||||
@@ -110,12 +92,6 @@ type Model struct {
|
|||||||
TerminalWidth int
|
TerminalWidth int
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
|
||||||
// ────────────────────────────────
|
|
||||||
// APP ENTRYPOINT
|
|
||||||
// ────────────────────────────────
|
|
||||||
//
|
|
||||||
|
|
||||||
func Run() error {
|
func Run() error {
|
||||||
p := tea.NewProgram(New(), tea.WithAltScreen())
|
p := tea.NewProgram(New(), tea.WithAltScreen())
|
||||||
_, err := p.Run()
|
_, err := p.Run()
|
||||||
@@ -125,8 +101,8 @@ func Run() error {
|
|||||||
func New() Model {
|
func New() Model {
|
||||||
base := BaseURLFromEnv()
|
base := BaseURLFromEnv()
|
||||||
client := NewClient(base, 15*time.Second)
|
client := NewClient(base, 15*time.Second)
|
||||||
|
|
||||||
styles := NewStyles()
|
styles := NewStyles()
|
||||||
|
|
||||||
m := Model{
|
m := Model{
|
||||||
apiClient: client,
|
apiClient: client,
|
||||||
styles: styles,
|
styles: styles,
|
||||||
@@ -162,12 +138,6 @@ func (m Model) Init() tea.Cmd {
|
|||||||
return tea.Batch(m.fetchSports(), m.fetchPopularMatches())
|
return tea.Batch(m.fetchSports(), m.fetchPopularMatches())
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
|
||||||
// ────────────────────────────────
|
|
||||||
// VIEW
|
|
||||||
// ────────────────────────────────
|
|
||||||
//
|
|
||||||
|
|
||||||
func (m Model) View() string {
|
func (m Model) View() string {
|
||||||
switch m.currentView {
|
switch m.currentView {
|
||||||
case viewHelp:
|
case viewHelp:
|
||||||
@@ -217,7 +187,7 @@ func (m Model) renderHelpPanel() string {
|
|||||||
|
|
||||||
panel := lipgloss.NewStyle().
|
panel := lipgloss.NewStyle().
|
||||||
Border(lipgloss.RoundedBorder()).
|
Border(lipgloss.RoundedBorder()).
|
||||||
BorderForeground(lipgloss.Color("#FA8072")). // salmon border
|
BorderForeground(lipgloss.Color("#FA8072")).
|
||||||
Padding(1, 2).
|
Padding(1, 2).
|
||||||
Width(int(float64(m.TerminalWidth) * 0.97)).
|
Width(int(float64(m.TerminalWidth) * 0.97)).
|
||||||
Render(sb.String())
|
Render(sb.String())
|
||||||
@@ -242,18 +212,18 @@ func (m Model) renderDebugPanel() string {
|
|||||||
return panel
|
return panel
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
|
||||||
// ────────────────────────────────
|
|
||||||
// UPDATE
|
|
||||||
// ────────────────────────────────
|
|
||||||
//
|
|
||||||
|
|
||||||
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
|
|
||||||
|
case debugLogMsg:
|
||||||
|
m.debugLines = append(m.debugLines, string(msg))
|
||||||
|
if len(m.debugLines) > 200 {
|
||||||
|
m.debugLines = m.debugLines[len(m.debugLines)-200:]
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
|
||||||
case tea.WindowSizeMsg:
|
case tea.WindowSizeMsg:
|
||||||
m.TerminalWidth = msg.Width
|
m.TerminalWidth = msg.Width
|
||||||
|
|
||||||
usableHeight := int(float64(msg.Height) * 0.9)
|
usableHeight := int(float64(msg.Height) * 0.9)
|
||||||
totalAvailableWidth := int(float64(msg.Width) * 0.97)
|
totalAvailableWidth := int(float64(msg.Width) * 0.97)
|
||||||
borderPadding := 4
|
borderPadding := 4
|
||||||
@@ -351,7 +321,10 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
case focusStreams:
|
case focusStreams:
|
||||||
if st, ok := m.streams.Selected(); ok {
|
if st, ok := m.streams.Selected(); ok {
|
||||||
return m, m.launchMPV(st)
|
return m, tea.Batch(
|
||||||
|
m.logToUI(fmt.Sprintf("Attempting extractor for %s", st.EmbedURL)),
|
||||||
|
m.runExtractor(st),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
@@ -364,26 +337,6 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|
||||||
case key.Matches(msg, m.keys.OpenMPV):
|
|
||||||
if m.focus == focusStreams {
|
|
||||||
if st, ok := m.streams.Selected(); ok {
|
|
||||||
go func(st Stream) {
|
|
||||||
m.debugLines = append(m.debugLines, fmt.Sprintf("Attempting extractor for %s", st.EmbedURL))
|
|
||||||
if err := m.forceMPVLaunch(st); err != nil {
|
|
||||||
m.lastError = err
|
|
||||||
m.debugLines = append(m.debugLines, fmt.Sprintf("Extractor failed: %v", err))
|
|
||||||
} else {
|
|
||||||
m.debugLines = append(m.debugLines, "Extractor success, launched MPV")
|
|
||||||
}
|
|
||||||
if len(m.debugLines) > 200 {
|
|
||||||
m.debugLines = m.debugLines[len(m.debugLines)-200:]
|
|
||||||
}
|
|
||||||
}(st)
|
|
||||||
m.status = fmt.Sprintf("🎞️ Attempting mpv: %s", st.EmbedURL)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|
||||||
@@ -415,12 +368,6 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
|
||||||
// ────────────────────────────────
|
|
||||||
// COMMANDS
|
|
||||||
// ────────────────────────────────
|
|
||||||
//
|
|
||||||
|
|
||||||
func (m Model) fetchSports() tea.Cmd {
|
func (m Model) fetchSports() tea.Cmd {
|
||||||
return func() tea.Msg {
|
return func() tea.Msg {
|
||||||
sports, err := m.apiClient.GetSports(context.Background())
|
sports, err := m.apiClient.GetSports(context.Background())
|
||||||
@@ -461,46 +408,35 @@ func (m Model) fetchStreamsForMatch(mt Match) tea.Cmd {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) launchMPV(st Stream) tea.Cmd {
|
func (m Model) runExtractor(st Stream) tea.Cmd {
|
||||||
return func() tea.Msg {
|
return func() tea.Msg {
|
||||||
url := st.EmbedURL
|
if st.EmbedURL == "" {
|
||||||
if url == "" {
|
return debugLogMsg("Extractor aborted: empty embed URL")
|
||||||
return errorMsg(fmt.Errorf("empty embedUrl for stream %s", st.ID))
|
|
||||||
}
|
}
|
||||||
cmd := exec.Command("mpv", "--no-terminal", "--really-quiet", url)
|
m3u8, err := extractM3U8Lite(st.EmbedURL, func(line string) {
|
||||||
|
// each extractor log line will flow back to the UI
|
||||||
|
return
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return debugLogMsg(fmt.Sprintf("Extractor failed: %v", err))
|
||||||
|
}
|
||||||
|
cmd := exec.Command("mpv",
|
||||||
|
"--no-terminal",
|
||||||
|
"--really-quiet",
|
||||||
|
fmt.Sprintf("--http-header-fields=User-Agent: %s", "Mozilla/5.0 (X11; Linux x86_64) Gecko/20100101 Firefox/144.0"),
|
||||||
|
fmt.Sprintf("--http-header-fields=Origin: %s", "https://embedsports.top"),
|
||||||
|
fmt.Sprintf("--http-header-fields=Referer: %s", "https://embedsports.top/"),
|
||||||
|
m3u8,
|
||||||
|
)
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
_ = cmd.Start()
|
_ = cmd.Start()
|
||||||
return launchStreamMsg{URL: url}
|
return debugLogMsg(fmt.Sprintf("Extractor success, launched MPV: %s", m3u8))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) forceMPVLaunch(st Stream) error {
|
func (m Model) logToUI(line string) tea.Cmd {
|
||||||
embed := strings.TrimSpace(st.EmbedURL)
|
return func() tea.Msg {
|
||||||
if embed == "" {
|
return debugLogMsg(line)
|
||||||
return fmt.Errorf("no embed URL for stream %s", st.ID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
m.debugLines = append(m.debugLines, fmt.Sprintf("[extractor] fetching %s", embed))
|
|
||||||
origin, referer, ua, err := deriveHeaders(embed)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("bad embed URL: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := fetchHTML(embed, ua, origin, referer, 12*time.Second)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("fetch failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
m3u8 := extractM3U8(body)
|
|
||||||
m.debugLines = append(m.debugLines, fmt.Sprintf("[extractor] yielded %s", m3u8))
|
|
||||||
if m3u8 == "" {
|
|
||||||
return fmt.Errorf("no .m3u8 found in embed page")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := launchMPV(m3u8, ua, origin, referer); err != nil {
|
|
||||||
return fmt.Errorf("mpv launch failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
179
internal/extractor.go
Normal file
179
internal/extractor.go
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// extractM3U8Lite performs static extraction like yt-dlp,
|
||||||
|
// logging each step to the provided debug callback (for the F12 panel).
|
||||||
|
func extractM3U8Lite(embedURL string, log func(string)) (string, error) {
|
||||||
|
const origin = "https://embedsports.top"
|
||||||
|
const referer = "https://embedsports.top/"
|
||||||
|
const ua = "Mozilla/5.0 (X11; Linux x86_64; rv:144.0) Gecko/20100101 Firefox/144.0"
|
||||||
|
|
||||||
|
if log != nil {
|
||||||
|
log(fmt.Sprintf("[extractor] Fetching embed page: %s", embedURL))
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := fetchHTML(embedURL, ua, origin, referer, 10*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
if log != nil {
|
||||||
|
log(fmt.Sprintf("[extractor] ❌ fetch failed: %v", err))
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("fetch embed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
k, i, s := parseEmbedVars(body)
|
||||||
|
if log != nil {
|
||||||
|
log(fmt.Sprintf("[extractor] Parsed vars → k=%q i=%q s=%q", k, i, s))
|
||||||
|
}
|
||||||
|
|
||||||
|
if k == "" || i == "" || s == "" {
|
||||||
|
if log != nil {
|
||||||
|
log("[extractor] ❌ missing vars (k,i,s) in embed HTML")
|
||||||
|
}
|
||||||
|
return "", errors.New("missing vars (k,i,s) in embed HTML")
|
||||||
|
}
|
||||||
|
|
||||||
|
if log != nil {
|
||||||
|
log("[extractor] Trying to extract secure token from bundle.js")
|
||||||
|
}
|
||||||
|
token, ok := extractTokenFromJS(origin+"/js/bundle.js", ua, origin, referer, log)
|
||||||
|
if ok {
|
||||||
|
if log != nil {
|
||||||
|
log(fmt.Sprintf("[extractor] Found token: %s", token))
|
||||||
|
}
|
||||||
|
for lb := 1; lb <= 9; lb++ {
|
||||||
|
url := fmt.Sprintf("https://lb%d.strmd.top/secure/%s/%s/stream/%s/%s/playlist.m3u8",
|
||||||
|
lb, token, k, i, s)
|
||||||
|
if log != nil {
|
||||||
|
log(fmt.Sprintf("[extractor] Probing %s", url))
|
||||||
|
}
|
||||||
|
if probeURL(url) {
|
||||||
|
if log != nil {
|
||||||
|
log(fmt.Sprintf("[extractor] ✅ Found working m3u8: %s", url))
|
||||||
|
}
|
||||||
|
return url, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if log != nil {
|
||||||
|
log("[extractor] ⚠️ No working load balancer found with token")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if log != nil {
|
||||||
|
log("[extractor] Fallback: probing default tokens")
|
||||||
|
}
|
||||||
|
for lb := 1; lb <= 9; lb++ {
|
||||||
|
testURL := fmt.Sprintf("https://lb%d.strmd.top/secure/test/%s/stream/%s/%s/playlist.m3u8",
|
||||||
|
lb, k, i, s)
|
||||||
|
if log != nil {
|
||||||
|
log(fmt.Sprintf("[extractor] Probing fallback %s", testURL))
|
||||||
|
}
|
||||||
|
if probeURL(testURL) {
|
||||||
|
if log != nil {
|
||||||
|
log(fmt.Sprintf("[extractor] ✅ Fallback succeeded: %s", testURL))
|
||||||
|
}
|
||||||
|
return testURL, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if log != nil {
|
||||||
|
log("[extractor] ❌ Unable to derive .m3u8 from embed")
|
||||||
|
}
|
||||||
|
return "", errors.New("unable to derive .m3u8")
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractTokenFromJS is modified to accept a logger callback
|
||||||
|
func extractTokenFromJS(url, ua, origin, referer string, log func(string)) (string, bool) {
|
||||||
|
if log != nil {
|
||||||
|
log(fmt.Sprintf("[extractor] Fetching JS: %s", url))
|
||||||
|
}
|
||||||
|
body, err := fetchHTML(url, ua, origin, referer, 8*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
if log != nil {
|
||||||
|
log(fmt.Sprintf("[extractor] ⚠️ fetch JS failed: %v", err))
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
if m := regexp.MustCompile(`https://lb\d+\.strmd\.top/secure/([A-Za-z0-9]+)/`).FindStringSubmatch(body); len(m) > 1 {
|
||||||
|
if log != nil {
|
||||||
|
log("[extractor] Found literal /secure/ token in JS")
|
||||||
|
}
|
||||||
|
return m[1], true
|
||||||
|
}
|
||||||
|
|
||||||
|
if m := regexp.MustCompile(`atob\("([A-Za-z0-9+/=]+)"\)`).FindStringSubmatch(body); len(m) > 1 {
|
||||||
|
if log != nil {
|
||||||
|
log("[extractor] Found base64 token pattern, decoding…")
|
||||||
|
}
|
||||||
|
decoded, err := base64.StdEncoding.DecodeString(m[1])
|
||||||
|
if err == nil {
|
||||||
|
tok := reverseString(string(decoded))
|
||||||
|
if len(tok) > 16 {
|
||||||
|
if log != nil {
|
||||||
|
log(fmt.Sprintf("[extractor] Extracted token (decoded+reversed): %s", tok))
|
||||||
|
}
|
||||||
|
return tok, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if m := regexp.MustCompile(`"([A-Za-z0-9]{24,})"\.split\(""\)\.reverse`).FindStringSubmatch(body); len(m) > 1 {
|
||||||
|
if log != nil {
|
||||||
|
log("[extractor] Found reversed string token")
|
||||||
|
}
|
||||||
|
return reverseString(m[1]), true
|
||||||
|
}
|
||||||
|
|
||||||
|
if log != nil {
|
||||||
|
log("[extractor] No token found in JS")
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseEmbedVars extracts var k, i, s from HTML
|
||||||
|
func parseEmbedVars(html string) (k, i, s string) {
|
||||||
|
reK := regexp.MustCompile(`var k\s*=\s*"([^"]+)"`)
|
||||||
|
reI := regexp.MustCompile(`var i\s*=\s*"([^"]+)"`)
|
||||||
|
reS := regexp.MustCompile(`var s\s*=\s*"([^"]+)"`)
|
||||||
|
|
||||||
|
if m := reK.FindStringSubmatch(html); len(m) > 1 {
|
||||||
|
k = m[1]
|
||||||
|
}
|
||||||
|
if m := reI.FindStringSubmatch(html); len(m) > 1 {
|
||||||
|
i = m[1]
|
||||||
|
}
|
||||||
|
if m := reS.FindStringSubmatch(html); len(m) > 1 {
|
||||||
|
s = m[1]
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// probeURL sends a quick HEAD to see if the URL exists
|
||||||
|
func probeURL(u string) bool {
|
||||||
|
client := &http.Client{Timeout: 4 * time.Second}
|
||||||
|
req, _ := http.NewRequest("HEAD", u, nil)
|
||||||
|
req.Header.Set("User-Agent", "Mozilla/5.0")
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
return resp.StatusCode == 200
|
||||||
|
}
|
||||||
|
|
||||||
|
// reverseString helper
|
||||||
|
func reverseString(s string) string {
|
||||||
|
r := []rune(s)
|
||||||
|
for i, j := 0, len(r)-1; i < j; i, j = i+1, j-1 {
|
||||||
|
r[i], r[j] = r[j], r[i]
|
||||||
|
}
|
||||||
|
return string(r)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user