Skip to content

Instantly share code, notes, and snippets.

@leonid-shevtsov
Created February 21, 2026 15:11
Show Gist options
  • Select an option

  • Save leonid-shevtsov/18d1e5165e22114cec1b910ba91f108b to your computer and use it in GitHub Desktop.

Select an option

Save leonid-shevtsov/18d1e5165e22114cec1b910ba91f108b to your computer and use it in GitHub Desktop.
// Telegram group video and photo downloader using gogram.
// Logs in with your account. Use -list to see your groups, then -group <id> to download videos and photos from that group.
package main
import (
"flag"
"fmt"
"log"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/amarnathcjd/gogram/telegram"
)
func main() {
listGroups := flag.Bool("list", false, "List groups you are in (use -group <id> to download from one)")
group := flag.String("group", "", "Group ID from -list, or @username (e.g. -1001234567890 or @channel)")
outputDir := flag.String("out", "./downloads", "Directory to save downloaded videos")
limit := flag.Int("limit", 100000, "Max number of messages to scan (0 = use default 100000)")
fromDate := flag.String("from", "", "Only download videos from this date onward (YYYY-MM-DD)")
toDate := flag.String("to", "", "Only download videos up to this date (YYYY-MM-DD)")
sessionPath := flag.String("session", "session", "Session file path for persistent login")
flag.Parse()
appID, appHash := os.Getenv("TELEGRAM_APP_ID"), os.Getenv("TELEGRAM_APP_HASH")
if appID == "" || appHash == "" {
log.Fatal("Set TELEGRAM_APP_ID and TELEGRAM_APP_HASH (get them from https://my.telegram.org)")
}
var appIDInt int32
if _, err := fmt.Sscanf(appID, "%d", &appIDInt); err != nil {
log.Fatalf("TELEGRAM_APP_ID must be a number: %v", err)
}
cfg := telegram.ClientConfig{
AppID: appIDInt,
AppHash: appHash,
Session: *sessionPath,
}
client, err := telegram.NewClient(cfg)
if err != nil {
log.Fatalf("NewClient: %v", err)
}
if _, err := client.Conn(); err != nil {
log.Fatalf("Connect: %v", err)
}
authorized, err := client.IsAuthorized()
if err != nil || !authorized {
if err := client.AuthPrompt(); err != nil {
log.Fatalf("Auth: %v", err)
}
}
if *listGroups {
listMyGroups(client)
return
}
peer := *group
if peer == "" {
log.Fatal("Use -list to see your groups, then -group <id> to download (e.g. -group -1001234567890)")
}
// If group looks like a 1-based index (1..10000), resolve from dialog list
if idx, err := strconv.Atoi(peer); err == nil && idx >= 1 && idx <= 10000 {
peer, err = resolveGroupByIndex(client, idx)
if err != nil {
log.Fatalf("Resolve group by index: %v", err)
}
}
if err := os.MkdirAll(*outputDir, 0755); err != nil {
log.Fatalf("Create output dir: %v", err)
}
msgLimit := int32(*limit)
if msgLimit <= 0 {
msgLimit = 100000
}
fromUnix, toUnix, err := parseDateRange(*fromDate, *toDate)
if err != nil {
log.Fatalf("Date range: %v", err)
}
var downloaded, skipped int
err = client.IterHistory(peer, func(msg *telegram.NewMessage) error {
var prefix string
var ext string
if msg.Video() != nil {
prefix = "video"
ext = getVideoExtension(msg)
} else if msg.Photo() != nil {
prefix = "photo"
ext = getPhotoExtension(msg)
} else {
return nil
}
msgTime := int64(msg.Date())
if fromUnix > 0 && msgTime < fromUnix {
return telegram.ErrStopIteration
}
if toUnix > 0 && msgTime > toUnix {
return nil
}
ts := time.Unix(msgTime, 0).Format("2006-01-02_15-04-05")
baseName := filepath.Join(*outputDir, fmt.Sprintf("%s_%s_%d", prefix, ts, msg.ID))
path := baseName + ext
if _, err := os.Stat(path); err == nil {
skipped++
return nil
}
_, err := msg.Download(&telegram.DownloadOptions{
FileName: path,
})
if err != nil {
log.Printf("Download msg %d: %v", msg.ID, err)
return nil
}
downloaded++
log.Printf("Downloaded: %s", path)
return nil
}, &telegram.HistoryOption{
Limit: msgLimit,
SleepThresholdMs: 50,
})
if err != nil {
log.Fatalf("IterHistory: %v", err)
}
log.Printf("Done. Downloaded: %d, skipped (already exists): %d", downloaded, skipped)
}
// parseDateRange parses -from and -to (YYYY-MM-DD). Returns (fromUnix, toUnix, error).
// fromUnix is start of day 00:00:00; toUnix is end of day 23:59:59. 0 means no bound.
func parseDateRange(fromStr, toStr string) (fromUnix, toUnix int64, err error) {
const layout = "2006-01-02"
if fromStr != "" {
t, e := time.Parse(layout, fromStr)
if e != nil {
return 0, 0, fmt.Errorf("-from: %w", e)
}
fromUnix = t.Unix()
}
if toStr != "" {
t, e := time.Parse(layout, toStr)
if e != nil {
return 0, 0, fmt.Errorf("-to: %w", e)
}
// End of day
toUnix = t.Add(24*time.Hour - time.Second).Unix()
}
return fromUnix, toUnix, nil
}
// listMyGroups fetches dialogs, filters to groups/supergroups, prints numbered list with ID and title.
func listMyGroups(c *telegram.Client) {
dialogs, err := c.GetDialogs(&telegram.DialogOptions{Limit: 500})
if err != nil {
log.Fatalf("GetDialogs: %v", err)
}
var groups []*telegram.TLDialog
for i := range dialogs {
d := &dialogs[i]
if d.IsChat() || d.IsChannel() {
groups = append(groups, d)
}
}
if len(groups) == 0 {
log.Println("No groups found.")
return
}
fmt.Println("Your groups (use -group <id> to download videos):")
fmt.Println()
for i, d := range groups {
title := ""
if d.IsChat() {
chat, err := d.GetChat(c)
if err == nil {
title = chat.Title
}
} else {
ch, err := d.GetChannel(c)
if err == nil {
title = ch.Title
if ch.Username != "" {
title += " (@" + ch.Username + ")"
}
}
}
if title == "" {
title = "(unknown)"
}
fmt.Printf(" %d. %s\n ID: %d\n\n", i+1, title, d.GetID())
}
fmt.Printf("Example: -group %d\n", groups[0].GetID())
}
// resolveGroupByIndex fetches dialogs, picks the 1-based index, returns peer ID as string.
func resolveGroupByIndex(c *telegram.Client, index int) (string, error) {
dialogs, err := c.GetDialogs(&telegram.DialogOptions{Limit: 500})
if err != nil {
return "", err
}
var groups []*telegram.TLDialog
for i := range dialogs {
d := &dialogs[i]
if d.IsChat() || d.IsChannel() {
groups = append(groups, d)
}
}
if index < 1 || index > len(groups) {
return "", fmt.Errorf("index %d out of range (1-%d)", index, len(groups))
}
return strconv.FormatInt(groups[index-1].GetID(), 10), nil
}
func getVideoExtension(msg *telegram.NewMessage) string {
doc := msg.Video()
if doc == nil {
return ".mp4"
}
for _, attr := range doc.Attributes {
if f, ok := attr.(*telegram.DocumentAttributeFilename); ok && f.FileName != "" {
ext := filepath.Ext(f.FileName)
if ext != "" {
return ext
}
break
}
}
if doc.MimeType != "" {
switch {
case strings.Contains(doc.MimeType, "webm"):
return ".webm"
case strings.Contains(doc.MimeType, "quicktime"), strings.Contains(doc.MimeType, "mp4"):
return ".mp4"
}
}
return ".mp4"
}
func getPhotoExtension(msg *telegram.NewMessage) string {
// Telegram photos are typically JPEG; server may not send a filename.
return ".jpg"
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment