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:
2026-06-25 14:22:48 +03:30
commit 05fcdf7458
26 changed files with 3302 additions and 0 deletions

View File

@@ -0,0 +1,22 @@
package domain
import "time"
const (
RateLimitInterval = 1 * time.Second
ProgressThreshold = 5.0
MaxFileSizeEnvKey = "MAX_FILE_SIZE_MB"
DefaultMaxFileSize = 2000
DefaultDailyQuotaMB = 100
DefaultWeeklyQuotaMB = 500
DefaultMonthlyQuotaMB = 2000
CleanupInterval = 10 * time.Second
FileRetention = 1 * time.Hour
DownloadDirEnvKey = "DOWNLOAD_DIR"
DefaultDownloadDir = "/tmp/uptodown"
CookiesFileEnvKey = "COOKIES_FILE"
DBPathEnvKey = "DB_PATH"
DefaultDBPath = "data/uptodown.db"
DateLayout = "2006-01-02"
QuotaProgressThreshold = 50.0
)

View File

@@ -0,0 +1,19 @@
package domain
import (
"net/url"
"strings"
)
// ValidateURL checks if the given string is a valid HTTP(S) URL.
func ValidateURL(raw string) (string, bool) {
raw = strings.TrimSpace(raw)
if raw == "" {
return "", false
}
u, err := url.Parse(raw)
if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" {
return "", false
}
return raw, true
}

View File

@@ -0,0 +1,26 @@
package domain
import "testing"
func TestValidateURL(t *testing.T) {
tests := []struct {
input string
want string
ok bool
}{
{"https://www.youtube.com/watch?v=123", "https://www.youtube.com/watch?v=123", true},
{"http://example.com/video", "http://example.com/video", true},
{"https://vimeo.com/123456", "https://vimeo.com/123456", true},
{"https://www.pornhub.com/view_video.php?viewkey=123", "https://www.pornhub.com/view_video.php?viewkey=123", true},
{"", "", false},
{"not a url", "", false},
{"ftp://example.com", "", false},
{" https://example.com ", "https://example.com", true},
}
for _, tt := range tests {
got, ok := ValidateURL(tt.input)
if ok != tt.ok || (ok && got != tt.want) {
t.Errorf("ValidateURL(%q) = (%q, %v), want (%q, %v)", tt.input, got, ok, tt.want, tt.ok)
}
}
}

163
internal/domain/types.go Normal file
View File

@@ -0,0 +1,163 @@
package domain
import "os/exec"
// MediaType represents the type of media the user wants to download.
type MediaType int
const (
MediaTypeAudio MediaType = iota + 1
MediaTypeVideo
MediaTypeVideoAudio
)
func (m MediaType) String() string {
switch m {
case MediaTypeAudio:
return "audio"
case MediaTypeVideo:
return "video"
case MediaTypeVideoAudio:
return "video_audio"
default:
return "unknown"
}
}
// Format represents a downloadable format from yt-dlp.
type Format struct {
ID string
Extension string
Width int
Height int
Resolution string
Filesize int64
Codec string
Bitrate string
Language string
IsAudioOnly bool
IsVideoOnly bool
HasAudio bool
HasVideo bool
}
// MediaInfo holds the full information about a downloadable media item.
type MediaInfo struct {
URL string
Title string
Duration float64
Formats []Format
Languages []string
IsPlaylist bool
PlaylistCount int
PlaylistEntries []PlaylistEntry
EstimatedSize int64
}
// PlaylistEntry represents a single entry in a playlist.
type PlaylistEntry struct {
ID string
Title string
URL string
Duration float64
}
// DownloadStatus represents the current state of a download job.
type DownloadStatus int
const (
StatusPending DownloadStatus = iota
StatusFetching
StatusDownloading
StatusProcessing
StatusCompleted
StatusFailed
StatusCancelled
)
func (s DownloadStatus) String() string {
switch s {
case StatusPending:
return "Pending"
case StatusFetching:
return "Fetching info"
case StatusDownloading:
return "Downloading"
case StatusProcessing:
return "Processing"
case StatusCompleted:
return "Completed"
case StatusFailed:
return "Failed"
case StatusCancelled:
return "Cancelled"
default:
return "Unknown"
}
}
// DownloadJob represents an active download process for a user.
type DownloadJob struct {
ID string
UserID int64
ChatID int64
MessageID int
URL string
MediaType MediaType
SelectedFormat *Format
Container string
Language string
Status DownloadStatus
Progress float64
Speed string
ETA string
FileSize int64
FilePath string
Title string
QuotaApplied bool
CancelCh chan struct{}
Cmd *exec.Cmd
}
// UserPreferences holds per-user default preferences.
type UserPreferences struct {
DefaultMediaType MediaType
DefaultQuality string
DefaultContainer string
DefaultLanguage string
}
// UserQuotas holds the quota usage and limits for a user.
type UserQuotas struct {
DailyLimitMB int64
DailyUsedMB int64
DailyDate string
WeeklyLimitMB int64
WeeklyUsedMB int64
WeekStart string
MonthlyLimitMB int64
MonthlyUsedMB int64
MonthStart string
}
// DownloadRecord is a completed download entry stored in history.
type DownloadRecord struct {
ID int64
UserID int64
URL string
Title string
MediaType string
FormatID string
Container string
FileSize int64
Status string
CreatedAt string
}
// PlaylistState tracks the user's current playlist selection flow.
type PlaylistState struct {
Entries []PlaylistEntry
Page int
PerPage int
Selected map[string]bool
}