added scheduling & media upload
This commit is contained in:
parent
f5c1694211
commit
3c23a975da
7 changed files with 431 additions and 7 deletions
230
upload.go
Normal file
230
upload.go
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
package twitterscraper
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
vidio "github.com/AlexEidt/Vidio"
|
||||
)
|
||||
|
||||
type Media struct {
|
||||
ID int
|
||||
Type string
|
||||
Size int
|
||||
Parts int
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
type uploadInitResponse struct {
|
||||
ID int `json:"media_id"`
|
||||
ExpiresAfter int `json:"expires_after_secs"`
|
||||
}
|
||||
|
||||
type ProcessingInfo struct {
|
||||
State string `json:"state"`
|
||||
CheckAfter int `json:"check_after_secs"`
|
||||
Progress int `json:"progress_percent"`
|
||||
}
|
||||
|
||||
type uploadStatusResponse struct {
|
||||
ProcessingInfo ProcessingInfo `json:"processing_info"`
|
||||
}
|
||||
|
||||
// Uploads photo, video or gif for further posting or scheduling. Expires in 24 hours if not used.
|
||||
func (s *Scraper) UploadMedia(filePath string) (*Media, error) {
|
||||
fileContent, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
media, err := s.uploadInit(filePath, fileContent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = s.uploadAppend(media, fileContent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var status *ProcessingInfo
|
||||
|
||||
status, err = s.uploadFinalize(media)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if strings.HasPrefix(media.Type, "image") {
|
||||
return media, nil
|
||||
}
|
||||
|
||||
for status.State != "succeeded" {
|
||||
time.Sleep(2 * time.Second)
|
||||
status, err = s.uploadStatus(media)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return media, nil
|
||||
}
|
||||
|
||||
func (s *Scraper) uploadInit(filePath string, fileContent []byte) (*Media, error) {
|
||||
var (
|
||||
videoDuration float64
|
||||
fileType string
|
||||
mediaCategory = "tweet_"
|
||||
)
|
||||
|
||||
fileType = http.DetectContentType(fileContent)
|
||||
|
||||
if fileType == "image/jpeg" || fileType == "image/png" {
|
||||
mediaCategory += "image"
|
||||
} else if fileType == "image/gif" {
|
||||
mediaCategory += "gif"
|
||||
} else if fileType == "video/mp4" || fileType == "video/quicktime" {
|
||||
mediaCategory += "video"
|
||||
|
||||
video, err := vidio.NewVideo(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
videoDuration = video.Duration()
|
||||
video.Close()
|
||||
} else {
|
||||
return nil, fmt.Errorf("file type %s unsupported by twitter, make sure you uploading photo, video or gif", fileType)
|
||||
}
|
||||
|
||||
req, err := s.newRequest("POST", "https://upload.twitter.com/i/media/upload.json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := url.Values{}
|
||||
query.Set("command", "INIT")
|
||||
query.Set("total_bytes", strconv.Itoa(len(fileContent)))
|
||||
query.Set("media_type", fileType)
|
||||
query.Set("media_category", mediaCategory)
|
||||
if mediaCategory == "tweet_video" {
|
||||
query.Set("video_duration_ms", strconv.FormatFloat(videoDuration*1000, 'f', -1, 64))
|
||||
}
|
||||
req.URL.RawQuery = query.Encode()
|
||||
req.Header.Set("Origin", "https://twitter.com")
|
||||
req.Header.Set("Referer", "https://twitter.com/")
|
||||
|
||||
var uploadInit uploadInitResponse
|
||||
|
||||
err = s.RequestAPI(req, &uploadInit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Media{
|
||||
ID: uploadInit.ID,
|
||||
Type: fileType,
|
||||
Size: len(fileContent),
|
||||
ExpiresAt: time.Now().Add(time.Duration(uploadInit.ExpiresAfter) * time.Second),
|
||||
Parts: len(fileContent) / 2_000_000,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Scraper) uploadAppend(media *Media, fileContent []byte) error {
|
||||
for i := 0; i <= media.Parts; i++ {
|
||||
var partData []byte
|
||||
|
||||
if i+1 <= media.Parts {
|
||||
partData = fileContent[i*2_000_000 : (i+1)*2_000_000]
|
||||
} else {
|
||||
partData = fileContent[i*2_000_000:]
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
w := multipart.NewWriter(&buf)
|
||||
fw, err := w.CreateFormFile("media", "blob")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if _, err = io.Copy(fw, bytes.NewReader(partData)); err != nil {
|
||||
return err
|
||||
}
|
||||
w.Close()
|
||||
|
||||
req, err := s.newRequest("POST", "https://upload.twitter.com/i/media/upload.json")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
query := url.Values{}
|
||||
query.Set("command", "APPEND")
|
||||
query.Set("media_id", strconv.Itoa(media.ID))
|
||||
query.Set("segment_index", strconv.Itoa(i))
|
||||
req.URL.RawQuery = query.Encode()
|
||||
req.Header.Set("Content-Type", w.FormDataContentType())
|
||||
req.Header.Set("Origin", "https://twitter.com")
|
||||
req.Header.Set("Referer", "https://twitter.com/")
|
||||
req.Body = io.NopCloser(&buf)
|
||||
|
||||
err = s.RequestAPI(req, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Scraper) uploadFinalize(media *Media) (*ProcessingInfo, error) {
|
||||
req, err := s.newRequest("POST", "https://upload.twitter.com/i/media/upload.json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := url.Values{}
|
||||
query.Set("command", "FINALIZE")
|
||||
query.Set("media_id", strconv.Itoa(media.ID))
|
||||
query.Set("allow_async", "true")
|
||||
req.URL.RawQuery = query.Encode()
|
||||
req.Header.Set("Origin", "https://twitter.com")
|
||||
req.Header.Set("Referer", "https://twitter.com/")
|
||||
|
||||
var response uploadStatusResponse
|
||||
|
||||
err = s.RequestAPI(req, &response)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &response.ProcessingInfo, nil
|
||||
}
|
||||
|
||||
func (s *Scraper) uploadStatus(media *Media) (*ProcessingInfo, error) {
|
||||
req, err := s.newRequest("GET", "https://upload.twitter.com/i/media/upload.json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := url.Values{}
|
||||
query.Set("command", "STATUS")
|
||||
query.Set("media_id", strconv.Itoa(media.ID))
|
||||
req.URL.RawQuery = query.Encode()
|
||||
req.Header.Set("Origin", "https://twitter.com")
|
||||
req.Header.Set("Referer", "https://twitter.com/")
|
||||
|
||||
var response uploadStatusResponse
|
||||
|
||||
err = s.RequestAPI(req, &response)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &response.ProcessingInfo, nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue