Some checks failed
StreamFlow CI/CD / Backend Tests (push) Has been cancelled
StreamFlow CI/CD / Backend Lint (push) Has been cancelled
StreamFlow CI/CD / Frontend Tests (push) Has been cancelled
StreamFlow CI/CD / Android TV Build (push) Has been cancelled
StreamFlow CI/CD / Docker Build (push) Has been cancelled
StreamFlow CI/CD / Docker Publish (push) Has been cancelled
127 lines
3.5 KiB
Go
127 lines
3.5 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/PuerkitoBio/goquery"
|
|
)
|
|
|
|
type VideoInfo struct {
|
|
Title string `json:"title"`
|
|
Thumbnail string `json:"thumbnail"`
|
|
Duration int `json:"duration"`
|
|
StreamURL string `json:"url"` // yt-dlp JSON key is 'url'
|
|
FormatID string `json:"format_id"`
|
|
Resolution string `json:"resolution"` // Custom field
|
|
Ext string `json:"ext"`
|
|
}
|
|
|
|
type VideoExtractor struct{}
|
|
|
|
func NewVideoExtractor() *VideoExtractor {
|
|
return &VideoExtractor{}
|
|
}
|
|
|
|
func (e *VideoExtractor) Extract(url string, quality string) (*VideoInfo, error) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
|
defer cancel()
|
|
|
|
// Check for custom extractors
|
|
if strings.Contains(url, "phim30.me") {
|
|
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create phim30 request: %v", err)
|
|
}
|
|
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
|
|
|
|
client := &http.Client{Timeout: 30 * time.Second}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to fetch phim30 page: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("phim30 returned status: %d", resp.StatusCode)
|
|
}
|
|
|
|
doc, err := goquery.NewDocumentFromReader(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse phim30 page: %v", err)
|
|
}
|
|
|
|
streamURL, _ := doc.Find("[data-movie-player-src-value]").Attr("data-movie-player-src-value")
|
|
if streamURL != "" {
|
|
return &VideoInfo{
|
|
StreamURL: streamURL,
|
|
Resolution: "unknown",
|
|
}, nil
|
|
}
|
|
return nil, fmt.Errorf("could not find stream URL on phim30 page")
|
|
}
|
|
|
|
// Build format selector
|
|
formatSelector := "bestvideo+bestaudio/best"
|
|
if quality != "" {
|
|
height := strings.Replace(quality, "p", "", -1)
|
|
formatSelector = fmt.Sprintf("bestvideo[height<=%s]+bestaudio/best[height<=%s]/best", height, height)
|
|
}
|
|
|
|
args := []string{
|
|
"--dump-json",
|
|
"--no-playlist",
|
|
"--no-warnings",
|
|
"--format", formatSelector,
|
|
url,
|
|
}
|
|
|
|
// Check for local yt-dlp.exe
|
|
ytDlpCmd := "yt-dlp"
|
|
// Only on windows for simplicity or check OS
|
|
if _, err := os.Stat("yt-dlp.exe"); err == nil {
|
|
path, _ := filepath.Abs("yt-dlp.exe")
|
|
ytDlpCmd = path
|
|
}
|
|
|
|
cmd := exec.CommandContext(ctx, ytDlpCmd, args...)
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("extraction failed: %v", err)
|
|
}
|
|
|
|
var info VideoInfo
|
|
// yt-dlp dumps JSON. Unmarshal it.
|
|
// Note: yt-dlp JSON has many fields, we only map the ones in VideoInfo struct
|
|
if err := json.Unmarshal(output, &info); err != nil {
|
|
return nil, fmt.Errorf("json parse error: %v", err)
|
|
}
|
|
|
|
// Post-process resolution if not directly available or custom logic needed
|
|
// In strict parsing, we might need a custom struct to catch 'height' and 'width' to form resolution
|
|
// allowing dynamic map parsing for simplicity:
|
|
var rawData map[string]interface{}
|
|
json.Unmarshal(output, &rawData)
|
|
|
|
if h, ok := rawData["height"].(float64); ok {
|
|
info.Resolution = fmt.Sprintf("%dp", int(h))
|
|
} else {
|
|
info.Resolution = "unknown"
|
|
}
|
|
|
|
// Ensure StreamURL is populated (sometimes 'url' is the stream url)
|
|
if info.StreamURL == "" {
|
|
if u, ok := rawData["url"].(string); ok {
|
|
info.StreamURL = u
|
|
}
|
|
}
|
|
|
|
return &info, nil
|
|
}
|