fix: resolve 8 architecture flaws across download, playlist, and webhook
All checks were successful
CI / build (push) Successful in 52s

- Add --continue and --max-filesize to yt-dlp for download resilience
- Route thumbnail downloads through HTTP_PROXY
- Show per-video progress (i/N) with cancel for playlist downloads
- Run playlist asynchronously so cancel callbacks can be processed
- Guard handlePlaylistConfirm against concurrent active jobs
- Close Done channel in playlist entry lifecycle
- Add stepState TTL (30 min) to prevent stale states
- Wrap webhook server for graceful shutdown on SIGTERM
- Refactor formatBytes to loop-based implementation
- Show first line of stderr in user-facing error messages
- Fix format pointer reuse in playlist loop
- Add CreatedAt field to stepState for TTL tracking
- Add MaxFileSize and ProgressPrefix fields to DownloadJob
This commit is contained in:
2026-06-26 01:04:34 +03:30
parent 57dffc4194
commit 609237f4e3
7 changed files with 181 additions and 69 deletions

View File

@@ -31,12 +31,20 @@ type stepState struct {
FormatIDs []string
PlaylistState *domain.PlaylistState
SearchState *domain.SearchState
CreatedAt time.Time
}
const stepStateTTL = 30 * time.Minute
func (h *Handler) getUserStepState(chatID int64) *stepState {
h.stepStatesMu.Lock()
defer h.stepStatesMu.Unlock()
return h.stepStates[chatID]
ss := h.stepStates[chatID]
if ss != nil && time.Since(ss.CreatedAt) > stepStateTTL {
delete(h.stepStates, chatID)
return nil
}
return ss
}
func (h *Handler) setUserStepState(chatID int64, ss *stepState) {
@@ -133,6 +141,7 @@ func (h *Handler) handleURLInput(ctx context.Context, chatID int64, text string,
URL: url,
MediaInfo: info,
Step: 0,
CreatedAt: time.Now(),
}
h.sendMediaPreview(ctx, chatID, info)
@@ -348,6 +357,7 @@ func (h *Handler) startDownload(ctx context.Context, chatID int64, msgID int, us
Status: domain.StatusDownloading,
Title: ss.MediaInfo.Title,
FileSize: fileSize,
MaxFileSize: h.maxFileSize,
Done: make(chan struct{}),
}
@@ -375,9 +385,13 @@ func (h *Handler) runDownload(ctx context.Context, job *domain.DownloadJob) {
defer func() { <-h.downloadSem }()
kb := progressKeyboard(0, "", "")
progressHeader := "Downloading: " + job.Title
if job.ProgressPrefix != "" {
progressHeader = job.ProgressPrefix + "\n" + progressHeader
}
msg, err := h.Bot.SendMessage(ctx, &tgbot.SendMessageParams{
ChatID: job.ChatID,
Text: fmt.Sprintf("Downloading: %s", job.Title),
Text: progressHeader,
ReplyMarkup: &kb,
})
if err != nil {
@@ -390,7 +404,9 @@ func (h *Handler) runDownload(ctx context.Context, job *domain.DownloadJob) {
if err != nil {
errMsg := err.Error()
if job.StderrLog != "" {
errMsg += ": " + job.StderrLog
if lines := strings.SplitN(job.StderrLog, "\n", 2); len(lines) > 0 && lines[0] != "" {
errMsg = strings.TrimSpace(lines[0])
}
}
h.editText(ctx, job.ChatID, job.MessageID, fmt.Sprintf("Download failed: %s", errMsg), nil)
job.Status = domain.StatusFailed
@@ -460,7 +476,10 @@ func (h *Handler) trackDownloadProgress(ctx context.Context, job *domain.Downloa
}
kb := progressKeyboard(upd.Progress, upd.Speed, upd.ETA)
text := fmt.Sprintf("Downloading: %s", job.Title)
text := "Downloading: " + job.Title
if job.ProgressPrefix != "" {
text = job.ProgressPrefix + "\n" + text
}
h.editText(ctx, job.ChatID, job.MessageID, text, &kb)
case <-ctx.Done():
@@ -732,7 +751,13 @@ func (h *Handler) sendMediaPreview(ctx context.Context, chatID int64, info *doma
// downloadFile downloads a URL to a temp file in the given directory and returns the path.
func downloadFile(url, dir string) (string, error) {
client := &http.Client{Timeout: 30 * time.Second}
tr := &http.Transport{
Proxy: http.ProxyFromEnvironment,
}
client := &http.Client{
Timeout: 30 * time.Second,
Transport: tr,
}
resp, err := client.Get(url)
if err != nil {
return "", fmt.Errorf("http get: %w", err)