feat: initial implementation of uptodownbot
Telegram bot for downloading media from any site using yt-dlp. Supports audio/video/both, quality selection, language picker, container format selection, playlist pagination, progress updates, per-user quotas, user preferences, download history, cancel at any step, polling and webhook modes, proxy support, and SQLite persistence.
This commit is contained in:
195
internal/handler/playlist.go
Normal file
195
internal/handler/playlist.go
Normal file
@@ -0,0 +1,195 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"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)
|
||||
|
||||
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,
|
||||
DefaultQuality: "best",
|
||||
DefaultContainer: "mp4",
|
||||
}
|
||||
}
|
||||
|
||||
format := &domain.Format{ID: prefs.DefaultQuality}
|
||||
for _, f := range info.Formats {
|
||||
if f.ID == prefs.DefaultQuality {
|
||||
format = &f
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
job := &domain.DownloadJob{
|
||||
ID: h.generateJobID(),
|
||||
UserID: userID,
|
||||
ChatID: chatID,
|
||||
URL: entry.URL,
|
||||
MediaType: prefs.DefaultMediaType,
|
||||
SelectedFormat: format,
|
||||
Container: prefs.DefaultContainer,
|
||||
Language: prefs.DefaultLanguage,
|
||||
Status: domain.StatusDownloading,
|
||||
Title: entry.Title,
|
||||
FileSize: format.Filesize,
|
||||
}
|
||||
|
||||
h.setActiveJob(userID, job)
|
||||
h.runDownload(ctx, job, 0)
|
||||
|
||||
// Wait for active job to complete
|
||||
for {
|
||||
j := h.getActiveJob(userID)
|
||||
if j == nil || j.Status == domain.StatusCompleted || j.Status == domain.StatusFailed || j.Status == domain.StatusCancelled {
|
||||
break
|
||||
}
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user