diff --git a/README.md b/README.md index cad8902..a529bcf 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,9 @@ Usage: -t, --tweet TWEET_ID Single tweet id to download -n, --nbr NBR Number of tweets to download -i, --img Download images only +-g, --gif Download GIFs only (need ffmpeg) -v, --video Download videos only --a, --all Download images and videos +-a, --all Download images, GIFs(need ffmpeg) and videos -r, --retweet Download retweet too -z, --url Print media url without download it -R, --retweet-only Donwload only retweet @@ -38,7 +39,7 @@ Usage: #### Download 300 tweets from @Spraytrains. -If the tweet doesn't contain a photo or video nothing will be downloaded but it will count towards the 300. +If the tweet doesn't contain a photo, gif or video nothing will be downloaded but it will count towards the 300. ```sh twmd -u Spraytrains -o ~/Downloads -a -n 300 @@ -57,6 +58,11 @@ You can use `-r|--retweet` to download retweets as well, or `-R|--retweet-only` twmd -t 156170319961391104 ``` +#### Download animated-GIFs: + +Animated-GIFs are downloaded as mp4 files. +If you want to get them as gif files, you need to install ffmpeg. + #### NSFW tweets You'll need to login `-L|--login` for downloading nsfw tweets. @@ -134,6 +140,3 @@ fi Check [here](https://gist.github.com/mmpx12/f0741d40909ed3f182fd6f9b33b580d7) for a full termux-url-opener example. - - -#### Gifs are not supported at the moment. diff --git a/twmd.go b/twmd.go index e541b97..31327bb 100644 --- a/twmd.go +++ b/twmd.go @@ -11,6 +11,7 @@ import ( "net/http" URL "net/url" "os" + "os/exec" "regexp" "strconv" "strings" @@ -32,6 +33,7 @@ var ( onlyrtw bool vidz bool imgs bool + gifs bool urlOnly bool version = "1.13.3" scraper *twitterscraper.Scraper @@ -75,6 +77,7 @@ func download(wg *sync.WaitGroup, tweet interface{}, url string, filetype string } var f *os.File + var outputDir string defer f.Close() if dwn_type == "user" { if update { @@ -84,11 +87,16 @@ func download(wg *sync.WaitGroup, tweet interface{}, url string, filetype string } } if filetype == "rtimg" { - f, _ = os.Create(output + "/img/RE-" + name) + outputDir = output + "/img/" + name = "RE-" + name + } else if filetype == "rtgif" { + outputDir = output + "/gif/" + name = "RE-" + name } else if filetype == "rtvideo" { - f, _ = os.Create(output + "/video/RE-" + name) + outputDir = output + "/video/" + name = "RE-" + name } else { - f, _ = os.Create(output + "/" + filetype + "/" + name) + outputDir = output + "/" + filetype + "/" } } else { if update { @@ -97,10 +105,39 @@ func download(wg *sync.WaitGroup, tweet interface{}, url string, filetype string return } } - f, _ = os.Create(output + "/" + name) + outputDir = output + "/" } + + f, _ = os.Create(outputDir + name) io.Copy(f, resp.Body) fmt.Println("Downloaded " + name) + + isGif := strings.Contains(url, "tweet_video") + if isGif { + _, err := exec.LookPath("ffmpeg") + if err != nil { + fmt.Println("Convert error: ", err) + return + } + gifName := strings.TrimSuffix(name, ".mp4") + ".gif" + + args := []string{ + "-y", + "-i", outputDir + name, + "-c:v", "gif", + outputDir + gifName, + } + + cmd := exec.Command("ffmpeg", args...) + err = cmd.Run() + if err != nil { + fmt.Println("Convert error: ", err) + return + } + + os.Remove(outputDir + name) + fmt.Println("Converted " + gifName) + } } func videoUser(wait *sync.WaitGroup, tweet *twitterscraper.TweetResult, output string, rt bool) { @@ -153,6 +190,24 @@ func photoUser(wait *sync.WaitGroup, tweet *twitterscraper.TweetResult, output s } } +func gifUser(wait *sync.WaitGroup, tweet *twitterscraper.TweetResult, output string, rt bool) { + defer wait.Done() + wg := sync.WaitGroup{} + if len(tweet.GIFs) > 0 || tweet.IsRetweet { + if tweet.IsRetweet && (rt || onlyrtw) { + singleTweet(output, tweet.ID) + } + for _, i := range tweet.GIFs { + if onlyrtw || tweet.IsRetweet { + continue + } + wg.Add(1) + go download(&wg, tweet, i.URL, "gif", output, "user") + } + wg.Wait() + } +} + func videoSingle(tweet *twitterscraper.Tweet, output string) { if tweet == nil { return @@ -180,7 +235,6 @@ func photoSingle(tweet *twitterscraper.Tweet, output string) { if len(tweet.Photos) > 0 { wg := sync.WaitGroup{} for _, i := range tweet.Photos { - fmt.Println(i.URL) var url string if !strings.Contains(i.URL, "video_thumb/") { if size == "orig" || size == "small" { @@ -201,6 +255,25 @@ func photoSingle(tweet *twitterscraper.Tweet, output string) { } } +func gifSingle(tweet *twitterscraper.Tweet, output string) { + if tweet == nil { + return + } + if len(tweet.GIFs) > 0 { + wg := sync.WaitGroup{} + for _, i := range tweet.GIFs { + if usr != "" { + wg.Add(1) + go download(&wg, tweet, i.URL, "rtgif", output, "user") + } else { + wg.Add(1) + go download(&wg, tweet, i.URL, "tweet", output, "tweet") + } + } + wg.Wait() + } +} + func askPass(loginp, twofa bool) { for { var username string @@ -270,9 +343,13 @@ func singleTweet(output string, id string) { if imgs { photoSingle(tweet, output) } + if gifs { + gifSingle(tweet, output) + } } else { videoSingle(tweet, output) photoSingle(tweet, output) + gifSingle(tweet, output) } } @@ -408,8 +485,9 @@ func main() { op.On("-t", "--tweet TWEET_ID", "Single tweet to download", &single) op.On("-n", "--nbr NBR", "Number of tweets to download", &nbr) op.On("-i", "--img", "Download images only", &imgs) + op.On("-g", "--gif", "Download GIFs only (need ffmpeg)", &gifs) op.On("-v", "--video", "Download videos only", &vidz) - op.On("-a", "--all", "Download images and videos", &all) + op.On("-a", "--all", "Download images, GIFs (need ffmpeg) and videos", &all) op.On("-r", "--retweet", "Download retweet too", &retweet) op.On("-z", "--url", "Print media url without download it", &urlOnly) op.On("-R", "--retweet-only", "Download only retweet", &onlyrtw) @@ -446,9 +524,10 @@ func main() { if all { vidz = true imgs = true + gifs = true } - if !vidz && !imgs && single == "" { - fmt.Println("You must specify what to download. (-i --img) for images, (-v --video) for videos or (-a --all) for both") + if !vidz && !imgs && !gifs && single == "" { + fmt.Println("You must specify what to download. (-i --img) for images, (-g --gif) for GIFs, (-v --video) for videos or (-a --all) for both") op.Help() os.Exit(1) } @@ -517,6 +596,9 @@ func main() { if imgs { os.MkdirAll(output+"/img", os.ModePerm) } + if gifs { + os.MkdirAll(output+"/gif", os.ModePerm) + } nbrs, _ := strconv.Atoi(nbr) wg := sync.WaitGroup{} for tweet := range scraper.GetTweets(context.Background(), usr, nbrs) { @@ -532,6 +614,10 @@ func main() { wg.Add(1) go photoUser(&wg, tweet, output, retweet) } + if gifs { + wg.Add(1) + go gifUser(&wg, tweet, output, retweet) + } } wg.Wait() }