All checks were successful
CI / build (push) Successful in 53s
- Split single sync.Mutex into per-map mutexes (activeJobs, stepStates, rateLimiter, pendingInput, cancelFuncs) to reduce contention - Add download semaphore (channel-buffered, 3 concurrent) as worker pool - Add cancelFuncs map with context cancellation wired into download goroutines for clean shutdown - Replace playlist busy-poll (500ms sleep loop) with synchronous runDownload + Done channel for completion tracking - Use full UUID for job IDs instead of 8-char prefix - Download thumbnail URL to temp file before sending (InputFileUpload) - Change MAX_FILE_SIZE_MB default from 2000 to 50 - Clean up cancel funcs after normal download completion
205 lines
5.1 KiB
Go
205 lines
5.1 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strconv"
|
|
|
|
"uptodownBot/internal/domain"
|
|
)
|
|
|
|
func (h *Handler) handlePlaylist(ctx context.Context, chatID int64, userID int64, url string, info *domain.MediaInfo) {
|
|
state := &domain.PlaylistState{
|
|
Entries: info.PlaylistEntries,
|
|
Page: 0,
|
|
PerPage: 10,
|
|
Selected: make(map[string]bool),
|
|
}
|
|
|
|
ss := &stepState{
|
|
URL: url,
|
|
MediaInfo: info,
|
|
PlaylistState: state,
|
|
}
|
|
h.setUserStepState(chatID, ss)
|
|
|
|
h.sendMediaPreview(ctx, chatID, info)
|
|
|
|
kb := playlistNavKeyboard(state, 0)
|
|
h.sendWithKB(ctx, chatID, fmt.Sprintf("Playlist: %s (%d videos)\n\nSelect videos to download:", info.Title, info.PlaylistCount), kb)
|
|
}
|
|
|
|
func (h *Handler) getPlaylistState(chatID int64) *domain.PlaylistState {
|
|
ss := h.getUserStepState(chatID)
|
|
if ss == nil {
|
|
return nil
|
|
}
|
|
return ss.PlaylistState
|
|
}
|
|
|
|
func (h *Handler) handlePlaylistPage(ctx context.Context, chatID int64, msgID int, userID int64, pageStr string) {
|
|
state := h.getPlaylistState(chatID)
|
|
if state == nil {
|
|
return
|
|
}
|
|
page, err := strconv.Atoi(pageStr)
|
|
if err != nil {
|
|
return
|
|
}
|
|
totalPages := (len(state.Entries) + state.PerPage - 1) / state.PerPage
|
|
if page < 0 || page >= totalPages {
|
|
return
|
|
}
|
|
state.Page = page
|
|
|
|
selectedCount := countSelected(state)
|
|
kb := playlistNavKeyboard(state, selectedCount)
|
|
ss := h.getUserStepState(chatID)
|
|
title := ""
|
|
if ss != nil {
|
|
title = ss.MediaInfo.Title
|
|
}
|
|
h.editText(ctx, chatID, msgID, fmt.Sprintf("Playlist: %s (%d videos)", title, len(state.Entries)), &kb)
|
|
}
|
|
|
|
func (h *Handler) handlePlaylistEntry(ctx context.Context, chatID int64, msgID int, userID int64, entryID string) {
|
|
state := h.getPlaylistState(chatID)
|
|
if state == nil {
|
|
return
|
|
}
|
|
state.Selected[entryID] = !state.Selected[entryID]
|
|
|
|
selectedCount := countSelected(state)
|
|
kb := playlistNavKeyboard(state, selectedCount)
|
|
ss := h.getUserStepState(chatID)
|
|
title := ""
|
|
if ss != nil {
|
|
title = ss.MediaInfo.Title
|
|
}
|
|
h.editText(ctx, chatID, msgID, fmt.Sprintf("Playlist: %s (%d videos)", title, len(state.Entries)), &kb)
|
|
}
|
|
|
|
func (h *Handler) handlePlaylistSelectAll(ctx context.Context, chatID int64, msgID int, userID int64) {
|
|
state := h.getPlaylistState(chatID)
|
|
if state == nil {
|
|
return
|
|
}
|
|
|
|
allSelected := true
|
|
for _, e := range state.Entries {
|
|
if !state.Selected[e.ID] {
|
|
allSelected = false
|
|
break
|
|
}
|
|
}
|
|
for _, e := range state.Entries {
|
|
state.Selected[e.ID] = !allSelected
|
|
}
|
|
|
|
selectedCount := 0
|
|
if !allSelected {
|
|
selectedCount = len(state.Entries)
|
|
}
|
|
kb := playlistNavKeyboard(state, selectedCount)
|
|
ss := h.getUserStepState(chatID)
|
|
title := ""
|
|
if ss != nil {
|
|
title = ss.MediaInfo.Title
|
|
}
|
|
h.editText(ctx, chatID, msgID, fmt.Sprintf("Playlist: %s (%d videos)", title, len(state.Entries)), &kb)
|
|
}
|
|
|
|
func (h *Handler) handlePlaylistConfirm(ctx context.Context, chatID int64, msgID int, userID int64) {
|
|
state := h.getPlaylistState(chatID)
|
|
if state == nil {
|
|
return
|
|
}
|
|
|
|
var selected []domain.PlaylistEntry
|
|
for _, e := range state.Entries {
|
|
if state.Selected[e.ID] {
|
|
selected = append(selected, e)
|
|
}
|
|
}
|
|
if len(selected) == 0 {
|
|
h.editText(ctx, chatID, msgID, "No videos selected.", nil)
|
|
return
|
|
}
|
|
|
|
h.clearStepState(chatID)
|
|
h.editText(ctx, chatID, msgID, fmt.Sprintf("Downloading %d videos sequentially...", len(selected)), nil)
|
|
|
|
for i, entry := range selected {
|
|
h.sendText(ctx, chatID, fmt.Sprintf("Downloading (%d/%d): %s", i+1, len(selected), entry.Title))
|
|
|
|
info, err := h.YtDlp.GetMediaInfo(entry.URL)
|
|
if err != nil {
|
|
h.sendText(ctx, chatID, fmt.Sprintf("Failed: %s", err.Error()))
|
|
continue
|
|
}
|
|
|
|
prefs, prefErr := h.Repo.GetOrCreatePreferences(userID)
|
|
if prefErr != nil {
|
|
prefs = &domain.UserPreferences{
|
|
DefaultMediaType: domain.MediaTypeVideoAudio,
|
|
DefaultAudioFormat: "best",
|
|
DefaultVideoFormat: "best",
|
|
DefaultAudioContainer: "mp3",
|
|
DefaultVideoContainer: "mp4",
|
|
}
|
|
}
|
|
|
|
formatID := prefs.DefaultVideoFormat
|
|
container := prefs.DefaultVideoContainer
|
|
if prefs.DefaultMediaType == domain.MediaTypeAudio {
|
|
formatID = prefs.DefaultAudioFormat
|
|
container = prefs.DefaultAudioContainer
|
|
}
|
|
format := &domain.Format{ID: formatID}
|
|
for _, f := range info.Formats {
|
|
if f.ID == formatID {
|
|
format = &f
|
|
break
|
|
}
|
|
}
|
|
|
|
dlCtx, dlCancel := context.WithCancel(context.Background())
|
|
|
|
job := &domain.DownloadJob{
|
|
ID: h.generateJobID(),
|
|
UserID: userID,
|
|
ChatID: chatID,
|
|
URL: entry.URL,
|
|
MediaType: prefs.DefaultMediaType,
|
|
SelectedFormat: format,
|
|
Container: container,
|
|
Language: prefs.DefaultLanguage,
|
|
Status: domain.StatusDownloading,
|
|
Title: entry.Title,
|
|
FileSize: format.Filesize,
|
|
Done: make(chan struct{}),
|
|
}
|
|
|
|
h.setActiveJob(userID, job)
|
|
h.setCancelFunc(userID, dlCancel)
|
|
|
|
h.runDownload(dlCtx, job)
|
|
|
|
// runDownload is synchronous — it blocks until the download completes.
|
|
// The Done channel is closed inside runDownload when it exits.
|
|
// No busy-polling needed.
|
|
}
|
|
|
|
h.sendText(ctx, chatID, "All playlist downloads completed.")
|
|
}
|
|
|
|
func countSelected(state *domain.PlaylistState) int {
|
|
count := 0
|
|
for _, s := range state.Selected {
|
|
if s {
|
|
count++
|
|
}
|
|
}
|
|
return count
|
|
}
|