fix: show bitrate for audio-only formats, filter subtitle languages, improve feedback
All checks were successful
CI / build (push) Successful in 50s

- Prioritize IsAudioOnly check in formatLabelForDisplay so bitrate
  is shown instead of yt-dlp's generic 'audio only' resolution
- Only collect languages from formats with audio/video to prevent
  subtitle-only tracks from triggering the language selection prompt
- Show 'Fetching metadata...' message before GetMediaInfo and edit
  with results, improving UX for URL input
- Add test coverage for audio-only format labels
This commit is contained in:
2026-06-26 12:12:33 +03:30
parent 464f20a46c
commit f5204b9554
3 changed files with 63 additions and 28 deletions

View File

@@ -171,7 +171,9 @@ func (c *Client) GetMediaInfo(url string) (*domain.MediaInfo, error) {
for _, f := range info.Formats { for _, f := range info.Formats {
ff := c.convertFormat(f) ff := c.convertFormat(f)
mi.Formats = append(mi.Formats, ff) mi.Formats = append(mi.Formats, ff)
if ff.Language != "" && !langSet[ff.Language] { // Only consider languages from formats with audio or video to avoid
// prompting for subtitle-only or auto-generated caption languages.
if (ff.HasAudio || ff.HasVideo) && ff.Language != "" && !langSet[ff.Language] {
langSet[ff.Language] = true langSet[ff.Language] = true
mi.Languages = append(mi.Languages, ff.Language) mi.Languages = append(mi.Languages, ff.Language)
} }

View File

@@ -72,6 +72,16 @@ func (h *Handler) handleURLInput(ctx context.Context, chatID int64, text string,
return return
} }
// Show a temporary status message that we will edit with results later.
fetchingMsg, _ := h.Bot.SendMessage(ctx, &tgbot.SendMessageParams{
ChatID: chatID,
Text: "Fetching metadata...",
})
fetchingMsgID := 0
if fetchingMsg != nil {
fetchingMsgID = fetchingMsg.ID
}
info, err := h.YtDlp.GetMediaInfo(url) info, err := h.YtDlp.GetMediaInfo(url)
if err != nil { if err != nil {
errMsg := err.Error() errMsg := err.Error()
@@ -86,57 +96,68 @@ func (h *Handler) handleURLInput(ctx context.Context, chatID int64, text string,
{{Text: "Cancel", CallbackData: "pending_cancel"}}, {{Text: "Cancel", CallbackData: "pending_cancel"}},
}, },
} }
h.Bot.SendMessage(ctx, &tgbot.SendMessageParams{ h.editText(ctx, chatID, fetchingMsgID, "This content is DRM protected and no YouTube alternative was found automatically. Try searching by name:", &kb)
ChatID: chatID, Text: "This content is DRM protected and no YouTube alternative was found automatically. Try searching by name:", ReplyMarkup: kb,
})
return return
} }
h.sendText(ctx, chatID, fmt.Sprintf("This content is DRM protected. Found on YouTube: %s", ytTitle)) h.editText(ctx, chatID, fetchingMsgID, fmt.Sprintf("This content is DRM protected. Found on YouTube: %s", ytTitle), nil)
info, err = h.YtDlp.GetMediaInfo(youtubeURL) info, err = h.YtDlp.GetMediaInfo(youtubeURL)
if err != nil { if err != nil {
h.sendText(ctx, chatID, fmt.Sprintf("YouTube fallback failed: %s", err.Error())) h.editText(ctx, chatID, fetchingMsgID, fmt.Sprintf("YouTube fallback failed: %s", err.Error()), nil)
return return
} }
url = youtubeURL url = youtubeURL
} else { } else {
switch { switch {
case strings.Contains(errMsg, "Unsupported URL") || strings.Contains(errMsg, "is not a valid URL"): case strings.Contains(errMsg, "Unsupported URL") || strings.Contains(errMsg, "is not a valid URL"):
h.sendText(ctx, chatID, "This URL is not supported.") h.editText(ctx, chatID, fetchingMsgID, "This URL is not supported.", nil)
case strings.Contains(errMsg, "HTTP Error 403") || strings.Contains(errMsg, "cookies"): case strings.Contains(errMsg, "HTTP Error 403") || strings.Contains(errMsg, "cookies"):
h.sendText(ctx, chatID, "This content requires authentication (cookies).") h.editText(ctx, chatID, fetchingMsgID, "This content requires authentication (cookies).", nil)
case strings.Contains(errMsg, "live"): case strings.Contains(errMsg, "live"):
h.sendText(ctx, chatID, "Cannot download live streams.") h.editText(ctx, chatID, fetchingMsgID, "Cannot download live streams.", nil)
default: default:
h.sendText(ctx, chatID, fmt.Sprintf("Error: %s", errMsg)) h.editText(ctx, chatID, fetchingMsgID, fmt.Sprintf("Error: %s", errMsg), nil)
} }
return return
} }
} }
if info.IsPlaylist { if info.IsPlaylist {
h.editText(ctx, chatID, fetchingMsgID, fmt.Sprintf("Playlist detected: %s (%d videos)", info.Title, info.PlaylistCount), nil)
h.handlePlaylist(ctx, chatID, userID, url, info) h.handlePlaylist(ctx, chatID, userID, url, info)
return return
} }
if len(info.Formats) == 0 { if len(info.Formats) == 0 {
h.sendText(ctx, chatID, "No formats available for this URL.") h.editText(ctx, chatID, fetchingMsgID, "No formats available for this URL.", nil)
return return
} }
// Reject if user has already exhausted quota (limit of 0 = unlimited). // Reject if user has already exhausted quota (limit of 0 = unlimited).
q, err := h.Repo.GetOrCreateQuotas(userID) q, qErr := h.Repo.GetOrCreateQuotas(userID)
if err == nil { if qErr == nil {
q = h.resetQuotasIfNeeded(q) q = h.resetQuotasIfNeeded(q)
if (q.DailyLimitMB > 0 && q.DailyUsedMB >= q.DailyLimitMB) || if (q.DailyLimitMB > 0 && q.DailyUsedMB >= q.DailyLimitMB) ||
(q.WeeklyLimitMB > 0 && q.WeeklyUsedMB >= q.WeeklyLimitMB) || (q.WeeklyLimitMB > 0 && q.WeeklyUsedMB >= q.WeeklyLimitMB) ||
(q.MonthlyLimitMB > 0 && q.MonthlyUsedMB >= q.MonthlyLimitMB) { (q.MonthlyLimitMB > 0 && q.MonthlyUsedMB >= q.MonthlyLimitMB) {
h.sendText(ctx, chatID, fmt.Sprintf( h.editText(ctx, chatID, fetchingMsgID, fmt.Sprintf(
"Quota exceeded. Daily: %d/%d MB, Weekly: %d/%d MB, Monthly: %d/%d MB.", "Quota exceeded. Daily: %d/%d MB, Weekly: %d/%d MB, Monthly: %d/%d MB.",
q.DailyUsedMB, q.DailyLimitMB, q.WeeklyUsedMB, q.WeeklyLimitMB, q.MonthlyUsedMB, q.MonthlyLimitMB)) q.DailyUsedMB, q.DailyLimitMB, q.WeeklyUsedMB, q.WeeklyLimitMB, q.MonthlyUsedMB, q.MonthlyLimitMB), nil)
return return
} }
} }
// Edit the fetching message to show the media title, then send a separate photo preview.
previewText := fmt.Sprintf("Title: %s", info.Title)
if info.Uploader != "" {
previewText += fmt.Sprintf("\nChannel: %s", info.Uploader)
}
if info.Duration > 0 {
previewText += fmt.Sprintf("\nDuration: %s", formatDuration(info.Duration))
}
h.editText(ctx, chatID, fetchingMsgID, previewText, nil)
h.sendMediaPreview(ctx, chatID, info)
ss := &stepState{ ss := &stepState{
URL: url, URL: url,
MediaInfo: info, MediaInfo: info,
@@ -144,8 +165,6 @@ func (h *Handler) handleURLInput(ctx context.Context, chatID int64, text string,
CreatedAt: time.Now(), CreatedAt: time.Now(),
} }
h.sendMediaPreview(ctx, chatID, info)
ss.FormatIDs, _ = buildFormatList(info.Formats) ss.FormatIDs, _ = buildFormatList(info.Formats)
h.setUserStepState(chatID, ss) h.setUserStepState(chatID, ss)
labels := make([]string, len(ss.FormatIDs)) labels := make([]string, len(ss.FormatIDs))
@@ -675,6 +694,19 @@ func findFormatByID(formats []domain.Format, id string) *domain.Format {
} }
func formatLabelForDisplay(f domain.Format) string { func formatLabelForDisplay(f domain.Format) string {
if f.IsAudioOnly {
label := f.Bitrate
if label == "" {
label = f.ID
if f.Extension != "" {
label += " (" + f.Extension + ")"
}
}
if f.Filesize > 0 {
label += fmt.Sprintf(" (%s)", formatBytes(f.Filesize))
}
return label
}
if f.Resolution != "" { if f.Resolution != "" {
label := f.Resolution label := f.Resolution
if f.Filesize > 0 { if f.Filesize > 0 {
@@ -689,17 +721,6 @@ func formatLabelForDisplay(f domain.Format) string {
} }
return label return label
} }
// Audio-only with no bitrate: show format ID + extension
if f.IsAudioOnly {
label := f.ID
if f.Extension != "" {
label += " (" + f.Extension + ")"
}
if f.Filesize > 0 {
label += fmt.Sprintf(" (%s)", formatBytes(f.Filesize))
}
return label
}
return f.ID return f.ID
} }

View File

@@ -194,6 +194,18 @@ func TestFormatLabelForDisplay(t *testing.T) {
f: domain.Format{ID: "137"}, f: domain.Format{ID: "137"},
want: "137", want: "137",
}, },
{
f: domain.Format{IsAudioOnly: true, Bitrate: "128k", Filesize: 1048576, Resolution: "audio only"},
want: "128k (1.0MB)",
},
{
f: domain.Format{IsAudioOnly: true, Bitrate: "128k", Resolution: "audio only"},
want: "128k",
},
{
f: domain.Format{IsAudioOnly: true, ID: "140", Extension: "m4a", Filesize: 1048576, Resolution: "audio only"},
want: "140 (m4a) (1.0MB)",
},
} }
for _, tt := range tests { for _, tt := range tests {
got := formatLabelForDisplay(tt.f) got := formatLabelForDisplay(tt.f)