-
-
Save leonid-shevtsov/18d1e5165e22114cec1b910ba91f108b to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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