fix: show bitrate for audio-only formats, filter subtitle languages, improve feedback
All checks were successful
CI / build (push) Successful in 50s
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:
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user