diff --git a/local-tool/server/internal/camera/video_source.go b/local-tool/server/internal/camera/video_source.go index 4502fa2..a26dd00 100644 --- a/local-tool/server/internal/camera/video_source.go +++ b/local-tool/server/internal/camera/video_source.go @@ -92,27 +92,53 @@ func NewVideoSourceFromURLWithSeek(rawURL string, fps float64, seekSeconds float // from platforms like YouTube, Vimeo, etc. // Returns the resolved direct URL or an error. func ResolveWithYTDLP(rawURL string) (string, error) { - // yt-dlp -f "best[ext=mp4]/best" --get-url cmd := exec.Command("yt-dlp", "-f", "best[ext=mp4]/best", "--get-url", rawURL) out, err := cmd.Output() if err != nil { if exitErr, ok := err.(*exec.ExitError); ok { - return "", fmt.Errorf("yt-dlp failed: %s", string(exitErr.Stderr)) + stderr := strings.TrimSpace(string(exitErr.Stderr)) + return "", fmt.Errorf("%s\n%s", friendlyYTDLPError(stderr), stderr) } - return "", fmt.Errorf("yt-dlp not available: %w", err) + return "", fmt.Errorf("yt-dlp 未安裝或無法執行: %w", err) } resolved := strings.TrimSpace(string(out)) if resolved == "" { - return "", fmt.Errorf("yt-dlp returned empty URL") + return "", fmt.Errorf("yt-dlp 無法取得影片下載連結(影片可能有地區限制或需要登入)") } - // yt-dlp may return multiple lines (video + audio); take only the first if idx := strings.Index(resolved, "\n"); idx > 0 { resolved = resolved[:idx] } return resolved, nil } +// friendlyYTDLPError 把 yt-dlp 的技術性錯誤訊息轉成使用者能理解的提示。 +func friendlyYTDLPError(stderr string) string { + s := strings.ToLower(stderr) + switch { + case strings.Contains(s, "sign in") || strings.Contains(s, "age"): + return "此影片需要登入 YouTube 帳號才能觀看(例如年齡限制),無法直接使用" + case strings.Contains(s, "private"): + return "此影片為私人影片,無法存取" + case strings.Contains(s, "unavailable") || strings.Contains(s, "not available"): + return "此影片無法取得(可能已被移除或在你的地區不可用)" + case strings.Contains(s, "copyright"): + return "此影片因版權限制無法下載" + case strings.Contains(s, "live"): + return "此影片為直播串流,目前不支援直播推論" + case strings.Contains(s, "premiere"): + return "此影片為首播(Premiere),尚未開始或格式不支援" + case strings.Contains(s, "drm") || strings.Contains(s, "protected"): + return "此影片有 DRM 保護,無法下載" + case strings.Contains(s, "rate limit") || strings.Contains(s, "429"): + return "YouTube 暫時限制了請求頻率,請稍後再試" + case strings.Contains(s, "no video formats"): + return "找不到可用的影片格式" + default: + return "無法解析此影片連結,請確認 URL 是否正確且影片為公開狀態" + } +} + func newVideoSource(input string, fps float64, isURL bool, seekSeconds float64) (*VideoSource, error) { if fps <= 0 { fps = 15