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:
22
internal/domain/constants.go
Normal file
22
internal/domain/constants.go
Normal 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
|
||||
)
|
||||
19
internal/domain/sanitize.go
Normal file
19
internal/domain/sanitize.go
Normal 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
|
||||
}
|
||||
26
internal/domain/sanitize_test.go
Normal file
26
internal/domain/sanitize_test.go
Normal 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
163
internal/domain/types.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user