package main import ( "encoding/json" "fmt" "log" "os" "os/exec" "path/filepath" "regexp" "strconv" "strings" ) const ( maxYouTubeDurationSec = 5 * 60 maxYouTubeFileBytes = 250 * 1024 * 1024 // лимит MAX для video ) var youtubeURLRe = regexp.MustCompile(`(?i)https?://(?:(?:www|m)\.)?(?:youtube\.com/(?:watch\?[^ \t\n]*v=|shorts/|embed/|live/)|youtu\.be/)[^\s<>"']+`) func extractYouTubeURL(text string) string { m := youtubeURLRe.FindString(text) if m == "" { return "" } return strings.TrimRight(m, ".,);]!?") } type ytDLPInfo struct { Duration float64 `json:"duration"` Title string `json:"title"` ID string `json:"id"` } func getYouTubeInfo(videoURL string) (*ytDLPInfo, error) { cmd := exec.Command("yt-dlp", "--dump-json", "--no-playlist", "--no-download", videoURL) out, err := cmd.Output() if err != nil { if ee, ok := err.(*exec.ExitError); ok { return nil, fmt.Errorf("yt-dlp info: %v: %s", err, string(ee.Stderr)) } return nil, fmt.Errorf("yt-dlp не найден или ошибка: %w", err) } var info ytDLPInfo if err := json.Unmarshal(out, &info); err != nil { return nil, err } return &info, nil } // downloadYouTubeIfShort скачивает ролик, если он короче 5 минут и уложится в лимит MAX. // Возвращает путь к временному файлу (caller должен удалить) или ошибку. func downloadYouTubeIfShort(videoURL string) (filePath string, title string, err error) { info, err := getYouTubeInfo(videoURL) if err != nil { return "", "", err } if info.Duration <= 0 { return "", "", fmt.Errorf("не удалось определить длительность") } if info.Duration > float64(maxYouTubeDurationSec) { return "", "", fmt.Errorf("видео длиннее 5 минут (%.0f с)", info.Duration) } tmpDir, err := os.MkdirTemp("", "bridge-yt-*") if err != nil { return "", "", err } outTmpl := filepath.Join(tmpDir, "video.%(ext)s") // mp4 предпочтительнее для MAX; ограничение размера 250 МБ cmd := exec.Command( "yt-dlp", "--no-playlist", "--max-filesize", strconv.Itoa(maxYouTubeFileBytes), "-f", "bv*[ext=mp4]+ba[ext=m4a]/b[ext=mp4]/bv*+ba/b", "--merge-output-format", "mp4", "-o", outTmpl, videoURL, ) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { _ = os.RemoveAll(tmpDir) return "", "", fmt.Errorf("yt-dlp download: %w", err) } entries, err := os.ReadDir(tmpDir) if err != nil || len(entries) == 0 { _ = os.RemoveAll(tmpDir) return "", "", fmt.Errorf("файл после скачивания не найден") } filePath = filepath.Join(tmpDir, entries[0].Name()) st, err := os.Stat(filePath) if err != nil { _ = os.RemoveAll(tmpDir) return "", "", err } if st.Size() > maxYouTubeFileBytes { _ = os.RemoveAll(tmpDir) return "", "", fmt.Errorf("файл больше лимита MAX 250 МБ (%d)", st.Size()) } log.Printf("[YouTube] скачано: %s (%.0f с, %d байт)", info.Title, info.Duration, st.Size()) return filePath, info.Title, nil } func cleanupYouTubeFile(filePath string) { if filePath == "" { return } _ = os.RemoveAll(filepath.Dir(filePath)) } // SendToMAXSmart: при YouTube-ссылке и ролике < 5 мин отправляет видео-вложение, иначе текст (со ссылкой). func (b *Bridge) SendToMAXSmart(chatID int64, text string) error { ytURL := extractYouTubeURL(text) if ytURL == "" { return b.SendToMAX(chatID, text) } filePath, title, err := downloadYouTubeIfShort(ytURL) if err != nil { log.Printf("[YouTube→MAX] не удалось скачать (%s), отправляю ссылкой: %v", ytURL, err) return b.SendToMAX(chatID, text) } defer cleanupYouTubeFile(filePath) caption := text if title != "" && !strings.Contains(text, title) { caption = text + "\n" + title } if err := b.SendVideoFileToMAX(chatID, filePath, caption); err != nil { log.Printf("[YouTube→MAX] ошибка загрузки видео, отправляю ссылкой: %v", err) return b.SendToMAX(chatID, text) } return nil }