diff --git a/api/client.go b/api/client.go index 5909e74..4e20bd6 100644 --- a/api/client.go +++ b/api/client.go @@ -1,6 +1,7 @@ package api import ( + "bytes" "encoding/json" "fmt" "io" @@ -82,11 +83,22 @@ func (c *Client) Get(path string, params map[string]string, target any) error { // Download fetches a URL and returns the raw bytes and MIME type. func Download(rawURL string) ([]byte, string, error) { + // fast-path for googlevideo.com to bypass throttling via chunked requests + if strings.Contains(rawURL, "googlevideo.com") { + return downloadChunked(rawURL) + } + req, err := http.NewRequest("GET", rawURL, nil) if err != nil { return nil, "", fmt.Errorf("download request: %w", err) } + // Add standard browser headers to avoid getting throttled/blocked + // by CDNs like googlevideo. + req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36") + req.Header.Set("DNT", "1") + req.Header.Set("Upgrade-Insecure-Requests", "1") + resp, err := httpClient.Do(req) if err != nil { return nil, "", fmt.Errorf("download: %w", err) @@ -115,5 +127,69 @@ func Download(rawURL string) ([]byte, string, error) { mime = mimetype.Detect(data).String() } + // fmt.Printf("Downloaded %d bytes from %s, MIME: %s\n", len(data), rawURL, mime) return data, mime, nil } + +func downloadChunked(rawURL string) ([]byte, string, error) { + chunkSize := 1024 * 1024 // 1MB + offset := 0 + + var buf bytes.Buffer + var mime string + + for { + req, err := http.NewRequest("GET", rawURL, nil) + if err != nil { + return nil, "", err + } + + req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36") + req.Header.Set("DNT", "1") + req.Header.Set("Upgrade-Insecure-Requests", "1") + req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", offset, offset+chunkSize-1)) + + resp, err := httpClient.Do(req) + if err != nil { + return nil, "", err + } + + if resp.StatusCode != 200 && resp.StatusCode != 206 { + if resp.StatusCode == 416 && offset > 0 { + resp.Body.Close() + break + } + resp.Body.Close() + return nil, "", fmt.Errorf("bad status: %d", resp.StatusCode) + } + + if offset == 0 { + mime = resp.Header.Get("Content-Type") + } + + data, err := io.ReadAll(io.LimitReader(resp.Body, int64(chunkSize+1))) + resp.Body.Close() + if err != nil { + return nil, "", err + } + + if int64(buf.Len()+len(data)) > MaxDownloadBytes { + return nil, "", fmt.Errorf("download too large: over %d bytes", MaxDownloadBytes) + } + + buf.Write(data) + + if len(data) < chunkSize || resp.StatusCode == 200 { + break + } + offset += len(data) + } + + detectedMime := mime + if detectedMime == "" || detectedMime == "application/octet-stream" { + detectedMime = mimetype.Detect(buf.Bytes()).String() + } + + // fmt.Printf("Downloaded %d chunks (%d bytes) from %s, MIME: %s\n", (offset/chunkSize)+1, buf.Len(), rawURL, detectedMime) + return buf.Bytes(), detectedMime, nil +} diff --git a/cmds/download/youtube.go b/cmds/download/youtube.go index dd81152..ba4524b 100644 --- a/cmds/download/youtube.go +++ b/cmds/download/youtube.go @@ -128,29 +128,50 @@ func formatSize(bytes int) string { } func downloadByFormatID(ctx *hc.Ctx, res *api.YtDownloadResult, fid string) { + var targetURL string + var isAudioOnly bool + // Look for video for _, f := range res.VideoFormats { if f.FormatID == fid && f.URL != "" { - data, mime, err := api.Download(f.URL) - if err != nil { - ctx.Reply(fmt.Sprintf("Download failed: %v", err)) - return - } - helper.SendMedia(ctx, data, mime, res.Title) // using existing sendMedia handling - return + targetURL = f.URL + break } } - // Look for audio - for _, f := range res.AudioFormats { - if f.FormatID == fid && f.URL != "" { - data, mime, err := api.Download(f.URL) - if err != nil { - ctx.Reply(fmt.Sprintf("Download failed: %v", err)) - return + + // Look for audio if not found in video + if targetURL == "" { + for _, f := range res.AudioFormats { + if f.FormatID == fid && f.URL != "" { + targetURL = f.URL + isAudioOnly = true + break } - helper.SendMedia(ctx, data, mime, res.Title) - return } } - ctx.Reply("Format " + fid + " not found") + + if targetURL == "" { + ctx.Reply("Format " + fid + " not found") + return + } + + data, mime, err := api.Download(targetURL) + if err != nil { + ctx.Reply(fmt.Sprintf("Download failed: %v", err)) + return + } + + // Detect if it is audio from googlevideo (which is often fragmented MP4/DASH), + // pipe it to ffmpeg to get standard MP3 so WhatsApp can play it + if isAudioOnly && strings.Contains(targetURL, "googlevideo.com") { + convertedData, err := helper.ConvertToMP3(data) + if err == nil { + data = convertedData + mime = "audio/mpeg" + } else { + fmt.Printf("Failed to convert audio using ffmpeg: %v\n", err) + } + } + + helper.SendMedia(ctx, data, mime, res.Title) } diff --git a/context/upload.go b/context/upload.go index 52de294..83f0d0a 100644 --- a/context/upload.go +++ b/context/upload.go @@ -47,13 +47,17 @@ func (c *Ctx) UploadVideo(bytes []byte, caption string) (*waE2E.VideoMessage, er } func (c *Ctx) UploadAudio(bytes []byte) (*waE2E.AudioMessage, error) { - mime := mimetype.Detect(bytes) + mime := mimetype.Detect(bytes).String() + if mime == "video/mp4" { + mime = "audio/mp4" + } + resp, err := c.client.Upload(stdctx.Background(), bytes, whatsmeow.MediaAudio) if err != nil { return nil, err } return &waE2E.AudioMessage{ - Mimetype: proto.String(mime.String()), + Mimetype: proto.String(mime), URL: &resp.URL, DirectPath: &resp.DirectPath, MediaKey: resp.MediaKey, diff --git a/helper/media.go b/helper/media.go index 3429173..412bdd6 100644 --- a/helper/media.go +++ b/helper/media.go @@ -1,11 +1,24 @@ package helper import ( + "bytes" + "os/exec" "strings" hc "neo/context" ) +func ConvertToMP3(data []byte) ([]byte, error) { + cmd := exec.Command("ffmpeg", "-i", "pipe:0", "-f", "mp3", "-ab", "128k", "pipe:1") + cmd.Stdin = bytes.NewReader(data) + var out bytes.Buffer + cmd.Stdout = &out + if err := cmd.Run(); err != nil { + return nil, err + } + return out.Bytes(), nil +} + func SendMedia(ctx *hc.Ctx, data []byte, mime, caption string) { switch { case strings.HasPrefix(mime, "video/mp4"):