From c0317225cab99ebc6b2de78d4835548e526a4e71 Mon Sep 17 00:00:00 2001 From: jim800121chen Date: Mon, 13 Apr 2026 00:56:15 +0800 Subject: [PATCH] =?UTF-8?q?feat(local-tool):=20yt-dlp=20=E9=8C=AF=E8=AA=A4?= =?UTF-8?q?=E8=A8=8A=E6=81=AF=E5=8F=8B=E5=96=84=E5=8C=96=20+=20=E8=BC=89?= =?UTF-8?q?=E5=85=A5=E9=80=BE=E6=99=82=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ResolveWithYTDLP 改善: - 新增 friendlyYTDLPError() 把 yt-dlp 的技術性 stderr 訊息轉成繁中提示 - 涵蓋 9 種常見失敗原因: 年齡限制 / 私人影片 / 已移除 / 版權 / 直播 / 首播 / DRM / 頻率限制 / 無格式 - 錯誤訊息同時附上原始 stderr(方便 debug) - yt-dlp 未安裝時改用繁中提示 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../server/internal/camera/video_source.go | 36 ++++++++++++++++--- 1 file changed, 31 insertions(+), 5 deletions(-) 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