├── .gitignore ├── README.md ├── adb └── adb.go ├── config └── config.go ├── controller └── download.go ├── dao ├── init.go ├── upload.go └── video.go ├── main.go ├── model ├── json.go ├── tag.go ├── upload.go └── video.go ├── service ├── download.go ├── feed.go └── hot.go ├── static ├── client_secret.json ├── config_base.toml ├── config_darwin.toml ├── config_linux.toml ├── create_table.sql ├── feed_example.json ├── mp4tots.bash └── rule_default.js ├── test └── go_test.go ├── utils ├── file.go ├── shell.go └── str.go └── youtube ├── authorize.go ├── cronjob.go ├── strategy.go └── upload.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.ear 17 | *.zip 18 | *.tar.gz 19 | *.rar 20 | .idea 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Douyin2Youtube 2 | ## Description 3 | This is a project that download videos from Douyin and upload the videos to Youtube automatic. 4 | 5 | I created the project to earn money from the Youtube video flow. But it is too late. 6 | There are nothing views of the video which download from Douyin.So I decide to open source it. 7 | 8 | Thanks for @[cnbattle/douyin](https://github.com/cnbattle/douyin)'s help 9 | 10 | ## Architecture 11 | The total architecture is next. 12 | 13 | ![Douyin2Youtube](https://i.niupic.com/images/2019/12/20/6anL.png) 14 | -------------------------------------------------------------------------------- /adb/adb.go: -------------------------------------------------------------------------------- 1 | package adb 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | "runtime" 7 | ) 8 | 9 | // Command 自定义参数 10 | func Command(arg ...string) { 11 | cmd := exec.Command(getAdbCli(), arg...) 12 | cmd.Stdout = os.Stdout 13 | _ = cmd.Run() 14 | } 15 | 16 | // RunApp 运行App 17 | func RunApp(startAppPath string) { 18 | cmd := exec.Command(getAdbCli(), "shell", "am", "start", "-n", startAppPath) 19 | cmd.Stdout = os.Stdout 20 | _ = cmd.Run() 21 | } 22 | 23 | // CloseApp 关闭 24 | func CloseApp(packageName string) { 25 | cmd := exec.Command(getAdbCli(), "shell", "am", "force-stop", packageName) 26 | cmd.Stdout = os.Stdout 27 | _ = cmd.Run() 28 | } 29 | 30 | // Swipe 滑动 31 | func Swipe(StartX, StartY, EndX, EndY string) { 32 | cmd := exec.Command(getAdbCli(), "shell", "input", "swipe", 33 | StartX, StartY, EndX, EndY, 34 | ) 35 | cmd.Stdout = os.Stdout 36 | _ = cmd.Run() 37 | } 38 | 39 | // InputText 输入文本 40 | func InputText(text string) { 41 | cmd := exec.Command(getAdbCli(), "shell", "input", "text", text) 42 | cmd.Stdout = os.Stdout 43 | _ = cmd.Run() 44 | } 45 | 46 | // InputKeyEvent 输入KeyEvent 47 | func InputKeyEvent(text string) { 48 | cmd := exec.Command(getAdbCli(), "shell", "input", "keyevent", text) 49 | cmd.Stdout = os.Stdout 50 | _ = cmd.Run() 51 | } 52 | 53 | // InputTextByADBKeyBoard 输入文本 54 | func InputTextByADBKeyBoard(text string) { 55 | cmd := exec.Command(getAdbCli(), "shell", "am", "broadcast", "-a", "ADB_INPUT_TEXT", "--es", "msg", text) 56 | cmd.Stdout = os.Stdout 57 | _ = cmd.Run() 58 | } 59 | 60 | // Click 点击某一像素点 61 | func Click(X, Y string) { 62 | cmd := exec.Command(getAdbCli(), "shell", "input", "tap", X, Y) 63 | cmd.Stdout = os.Stdout 64 | _ = cmd.Run() 65 | } 66 | 67 | // ClickKeyCode 点击android对应的keycode 68 | func ClickKeyCode(code string) { 69 | cmd := exec.Command(getAdbCli(), "shell", "input", "keyevent", code) 70 | cmd.Stdout = os.Stdout 71 | _ = cmd.Run() 72 | } 73 | 74 | // ClickHome 点击hone键 75 | func ClickHome() { 76 | ClickKeyCode("3") 77 | } 78 | 79 | // ClickHome 点击返回键 80 | func ClickBack() { 81 | ClickKeyCode("4") 82 | } 83 | 84 | // getAdbCli 获取adb cli 85 | func getAdbCli() string { 86 | if runtime.GOOS == "windows" { 87 | return "./static/adb/adb.exe" 88 | } 89 | return "adb" 90 | } 91 | 92 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/spf13/viper" 5 | "runtime" 6 | "time" 7 | ) 8 | 9 | var configBaseName, configType = "config_base", "toml" 10 | var configPath, configName = "", "" 11 | 12 | var BaseConfig *viper.Viper 13 | var SpecifyConfig *viper.Viper 14 | 15 | 16 | func init() { 17 | var platform = runtime.GOOS 18 | if platform == "darwin"{ 19 | configPath = "/Users/shiwenhao/Documents/Project/My/douyin2youtube/src/project/static" 20 | configName = "config_darwin" 21 | }else if platform == "linux"{ 22 | configPath = "/home/shiwenhao/Documents//Project/My/douyin2youtube/src/project/static" 23 | configName = "config_linux" 24 | } 25 | 26 | BaseConfig = viper.New() 27 | BaseConfig.SetConfigName(configBaseName) 28 | BaseConfig.AddConfigPath(configPath) 29 | BaseConfig.SetConfigType(configType) 30 | if err := BaseConfig.ReadInConfig(); err != nil { 31 | panic(err) 32 | } 33 | 34 | SpecifyConfig = viper.New() 35 | SpecifyConfig.SetConfigName(configName) 36 | SpecifyConfig.AddConfigPath(configPath) 37 | SpecifyConfig.SetConfigType(configType) 38 | 39 | if err := SpecifyConfig.ReadInConfig(); err != nil { 40 | panic(err) 41 | } 42 | } 43 | 44 | 45 | func GetString(key string) string{ 46 | res := SpecifyConfig.GetString(key) 47 | if res != ""{ 48 | return res 49 | } 50 | return BaseConfig.GetString(key) 51 | } 52 | 53 | func GetInt(key string) int{ 54 | res := SpecifyConfig.GetInt(key) 55 | if res != 0{ 56 | return res 57 | } 58 | return BaseConfig.GetInt(key) 59 | } 60 | 61 | func GetDuration(key string) time.Duration{ 62 | res := SpecifyConfig.GetDuration(key) 63 | if res != 0{ 64 | return res 65 | } 66 | return BaseConfig.GetDuration(key) 67 | } 68 | -------------------------------------------------------------------------------- /controller/download.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/gin-gonic/gin" 7 | "project/model" 8 | "project/service" 9 | ) 10 | 11 | func DownloadFeed(ctx *gin.Context){ 12 | body := ctx.DefaultPostForm("json", "null") 13 | var feedJson model.FeedJson 14 | err := json.Unmarshal([]byte(body), &feedJson) 15 | if err != nil{ 16 | fmt.Println(err) 17 | } 18 | 19 | service.HandleFeed(feedJson) 20 | ctx.JSON(200, gin.H{"status": 0, "message": "success"}) 21 | } 22 | 23 | func DownloadHot(ctx *gin.Context){ 24 | 25 | } 26 | 27 | func DownloadSearch(ctx *gin.Context){ 28 | 29 | } 30 | -------------------------------------------------------------------------------- /dao/init.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import ( 4 | "fmt" 5 | "github.com/astaxie/beego/orm" 6 | _ "github.com/go-sql-driver/mysql" 7 | "project/config" 8 | "project/model" 9 | "strings" 10 | ) 11 | 12 | func init(){ 13 | _ = orm.RegisterDriver(config.GetString("database.driver_name"), orm.DRMySQL) 14 | dataBase := config.GetString("database.user_name") + ":" + 15 | config.GetString("database.password") + "@tcp(" + config.GetString("database.ip") + ":" + 16 | config.GetString("database.port") + ")/" + config.GetString("database.db_name") 17 | fmt.Println(dataBase) 18 | err := orm.RegisterDataBase("default", config.GetString("database.driver_name"), dataBase) 19 | if err != nil{ 20 | fmt.Println(err) 21 | } 22 | 23 | orm.RegisterModel(new(model.Video), new(model.VideoInfo), new(model.VideoTagRef), new(model.Tag), 24 | new(model.UploadVideoRef), new(model.Upload), new(model.UploadInfo)) 25 | } 26 | 27 | 28 | func CheckDuplicateKey(err error) bool{ 29 | return strings.Contains(err.Error(), "duplicate") 30 | } -------------------------------------------------------------------------------- /dao/upload.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import ( 4 | "fmt" 5 | "github.com/astaxie/beego/orm" 6 | "project/model" 7 | "strconv" 8 | ) 9 | 10 | func QueryUploadedVideo(o orm.Ormer) []string{ 11 | query := "SELECT distinct(upload_video_ref.video_id) FROM upload inner join upload_video_ref on " + 12 | "upload.upload_id = upload_video_ref.upload_id where upload.upload_result = 1" 13 | 14 | var res orm.ParamsList 15 | videoIds := make([]string, 0) 16 | _, err := o.Raw(query).ValuesFlat(&res) 17 | if err != nil{ 18 | fmt.Println(err) 19 | } 20 | for _, singleRes := range res{ 21 | videoIds = append(videoIds, singleRes.(string)) 22 | } 23 | return videoIds 24 | 25 | } 26 | 27 | func InsertUploadVideoRefs(uploadVideoRefs []model.UploadVideoRef , o orm.Ormer) error{ 28 | inserter, _ := o.QueryTable("upload_video_ref").PrepareInsert() 29 | var err error = nil 30 | for index, _ := range uploadVideoRefs{ 31 | _, err = inserter.Insert(&uploadVideoRefs[index]) 32 | if err != nil && !CheckDuplicateKey(err){ 33 | fmt.Println(err) 34 | } 35 | } 36 | return err 37 | } 38 | 39 | func QueryOneNotUploadVideo(o orm.Ormer) *model.UploadInfo{ 40 | query := "select * from upload_info left join upload " + 41 | "on upload.upload_id = upload_info.upload_id " + 42 | "where upload_result IS NULL or upload_result = 1 limit 1;" 43 | 44 | var uploadInfo = new(model.UploadInfo) 45 | 46 | var lists []orm.ParamsList 47 | num, err := o.Raw(query).ValuesList(&lists) 48 | if err != nil{ 49 | panic(err) 50 | } 51 | if num <= 0 { 52 | return uploadInfo 53 | } 54 | uploadInfo.Id, _ = strconv.Atoi(lists[0][0].(string)) 55 | err = o.Read(uploadInfo) 56 | return uploadInfo 57 | } -------------------------------------------------------------------------------- /dao/video.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import ( 4 | "fmt" 5 | "project/model" 6 | "github.com/astaxie/beego/orm" 7 | ) 8 | 9 | func InsertVideos(videos []model.Video, o orm.Ormer) error{ 10 | inserter, _ := o.QueryTable("video").PrepareInsert() 11 | var err error = nil 12 | for index, _ := range videos{ 13 | _, err = inserter.Insert(&videos[index]) 14 | if err != nil && !CheckDuplicateKey(err){ 15 | fmt.Println(err) 16 | } 17 | } 18 | return err 19 | } 20 | 21 | func InsertVideoInfos(videoInfos []model.VideoInfo, o orm.Ormer) error{ 22 | inserter, _ := o.QueryTable("video_info").PrepareInsert() 23 | var err error = nil 24 | for index, _ := range videoInfos{ 25 | _, err = inserter.Insert(&videoInfos[index]) 26 | if err != nil && !CheckDuplicateKey(err){ 27 | fmt.Println(err) 28 | } 29 | } 30 | return err 31 | } 32 | 33 | 34 | //查询没在范围内的video id 35 | func QueryNotUploadVideo(videoIds []string, o orm.Ormer) []model.VideoInfo{ 36 | filterVideosQuery := "select * from video_info where has_download = 1" 37 | filterVideoIds := "(" 38 | for index, videoId := range videoIds{ 39 | if index != len(videoIds) - 1 { 40 | filterVideoIds += videoId + ", " 41 | }else{ 42 | filterVideoIds += videoId + ")" 43 | } 44 | } 45 | if len(videoIds) > 0{ 46 | filterVideosQuery += "and video_id not in" + filterVideoIds 47 | } 48 | var videoInfos []model.VideoInfo 49 | _, err := o.Raw( filterVideosQuery).QueryRows(&videoInfos) 50 | if err != nil{ 51 | fmt.Print(err) 52 | } 53 | return videoInfos 54 | } 55 | 56 | // 57 | func QueryVideoInfosById(videoIds []string, o orm.Ormer) []model.VideoInfo{ 58 | filterVideosQuery := "(" 59 | for index, videoId := range videoIds{ 60 | if index != len(videoIds) - 1 { 61 | filterVideosQuery += videoId + ", " 62 | }else{ 63 | filterVideosQuery += videoId + ")" 64 | } 65 | } 66 | var videoInfos []model.VideoInfo 67 | _, err := o.Raw(filterVideosQuery).QueryRows(&filterVideosQuery) 68 | if err != nil{ 69 | fmt.Print(err) 70 | } 71 | return videoInfos 72 | } -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/astaxie/beego/orm" 6 | "github.com/gin-gonic/gin" 7 | "os" 8 | "os/exec" 9 | "project/adb" 10 | "project/config" 11 | "project/controller" 12 | "project/dao" 13 | _ "project/dao" 14 | "project/model" 15 | "project/youtube" 16 | "strconv" 17 | "time" 18 | ) 19 | 20 | func WebStart(){ 21 | gin.SetMode(config.GetString("gin.model")) 22 | r := gin.Default() 23 | r.POST("/feed/", controller.DownloadFeed) 24 | _ = r.Run(":" + strconv.Itoa(config.GetInt("gin.addr"))) 25 | } 26 | 27 | func AnyProxyStart(){ 28 | cmd := exec.Command("anyproxy", "--intercept", "--rule", 29 | config.GetString("anyproxy.rule_js_path")) 30 | cmd.Stdout = os.Stdout 31 | cmd.Stderr = os.Stderr 32 | _ = cmd.Run() 33 | } 34 | 35 | func AdbStart(){ 36 | adb.CloseApp(config.GetString("app.packageName")) 37 | adb.RunApp(config.GetString("app.packageName") + "/" + config.GetString("app.startPath")) 38 | for { 39 | adb.Swipe(config.GetString("swipe.startX"), config.GetString("swipe.startY"), 40 | config.GetString("swipe.endX"), config.GetString("swipe.endY")) 41 | time.Sleep(config.GetDuration("swipe.sleep") * time.Millisecond) 42 | } 43 | } 44 | 45 | func CombineCronJob(){ 46 | ticker := time.NewTicker(30 * 60 * time.Second) 47 | defer ticker.Stop() 48 | for { 49 | select { 50 | case <-ticker.C: 51 | youtube.Task() 52 | } 53 | } 54 | } 55 | 56 | func UploadCronJob(){ 57 | o := orm.NewOrm() 58 | uploadInfo := dao.QueryOneNotUploadVideo(o) 59 | err := youtube.Upload(*uploadInfo) 60 | if err == nil{ 61 | var upload = new(model.Upload) 62 | upload.UploadId = uploadInfo.UploadId 63 | upload.UploadResult = 0 64 | _, err = o.Insert(uploadInfo) 65 | if err != nil{ 66 | fmt.Printf(err.Error()) 67 | } 68 | } 69 | 70 | 71 | } 72 | 73 | func main(){ 74 | go AnyProxyStart() 75 | go WebStart() 76 | //go AdbStart() 77 | //go CombineCronJob() 78 | //UploadCronJob() 79 | select {} 80 | } 81 | -------------------------------------------------------------------------------- /model/json.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | type FeedJson struct { 9 | StatusCode int `json:"status_code"` 10 | MinCursor int `json:"min_cursor"` 11 | MaxCursor int `json:"max_cursor"` 12 | HasMore int `json:"has_more"` 13 | AwemeList []Aweme `json:"aweme_list"` 14 | } 15 | 16 | type Aweme struct { 17 | AwemeId string `json:"aweme_id"` 18 | Desc string `json:"desc"` 19 | CreateTime int `json:"create_time"` 20 | Author author `json:"author"` 21 | Video video `json:"video"` 22 | IsAds bool `json:"is_ads"` 23 | Duration int `json:"duration"` 24 | GroupId string `json:"group_id"` 25 | AuthorUserId int64 `json:"author_user_id"` 26 | LongVideo []longVideo `json:"long_video"` 27 | Statistics statistics `json:"statistics"` 28 | ShareInfo shareInfo `json:"share_info"` 29 | } 30 | 31 | type author struct { 32 | Uid string `json:"uid"` 33 | Nickname string `json:"nickname"` 34 | Gender int `json:"gender"` 35 | UniqueId string `json:"unique_id"` 36 | AvatarThumb uriStr `json:"avatar_thumb"` 37 | } 38 | 39 | type video struct { 40 | PlayAddr uriStr `json:"play_addr"` 41 | Cover uriStr `json:"cover"` 42 | Duration int `json:"duration"` 43 | DownloadAddr uriStr `json:"download_addr"` 44 | } 45 | 46 | type longVideo struct { 47 | Video video `json:"video"` 48 | TrailerStartTime int `json:"trailer_start_time"` 49 | } 50 | 51 | type uriStr struct { 52 | Uri string `json:"uri"` 53 | UrlList []string `json:"url_list"` 54 | Width int `json:"width"` 55 | Height int `json:"height"` 56 | } 57 | 58 | type statistics struct { 59 | AwemeId string `json:"aweme_id"` 60 | CommentCount int `json:"comment_count"` 61 | LikeCount int `json:"digg_count"` 62 | DownloadCount int `json:"download_count"` 63 | PlayCount int `json:"play_count"` 64 | ShareCount int `json:"share_count"` 65 | ForwardCount int `json:"forward_count"` 66 | LoseCount int `json:"lose_count"` 67 | LoseCommentCount int `json:"lose_comment_count"` 68 | } 69 | 70 | type shareInfo struct { 71 | ShareUrl string `json:"share_url"` 72 | ShareTitle string `json:"share_title"` 73 | } 74 | 75 | func (aweme *Aweme) LoadVideo(video *Video){ 76 | video.VideoId = aweme.Video.PlayAddr.Uri 77 | video.AuthorId = aweme.Author.Uid 78 | } 79 | 80 | func (aweme *Aweme) LoadVideoInfo(videoInfo *VideoInfo){ 81 | videoInfo.VideoId = aweme.Video.PlayAddr.Uri 82 | videoInfo.Description = aweme.Desc 83 | videoInfo.Duration = aweme.Video.Duration 84 | videoInfo.CommentCount = aweme.Statistics.CommentCount 85 | videoInfo.LikeCount = aweme.Statistics.LikeCount 86 | urlListData, err := json.Marshal(aweme.Video.DownloadAddr.UrlList) 87 | if err == nil{ 88 | videoInfo.UrlList = string(urlListData) 89 | } else{ 90 | fmt.Println(err) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /model/tag.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | _ "github.com/astaxie/beego/orm" 5 | ) 6 | 7 | type Tag struct{ 8 | Id int `orm:"column(id)"` 9 | Name string `orm:"column(name)"` 10 | } 11 | -------------------------------------------------------------------------------- /model/upload.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | _ "github.com/astaxie/beego/orm" 5 | "time" 6 | ) 7 | 8 | type Upload struct{ 9 | Id int `orm:"column(id)"` 10 | UploadId string `orm:"column(upload_id)"` 11 | CreateTime time.Time `orm:"column(create_time)"` 12 | UploadResult int `orm:"column(upload_result)"` 13 | } 14 | 15 | type UploadInfo struct{ 16 | Id int `orm:"column(id)"` 17 | UploadId string `orm:"column(upload_id)"` 18 | Title string `orm:"column(title)"` 19 | Description string `orm:"column(description)"` 20 | Category string `orm:"column(category)"` 21 | Keywords string `orm:"column(keywords)"` 22 | Privacy string `orm:"column(privacy)"` 23 | VideoPath string `orm:"column(video_path)"` 24 | } 25 | 26 | type UploadVideoRef struct{ 27 | Id int `orm:"column(id)"` 28 | UploadId string `orm:"column(upload_id)"` 29 | VideoId string `orm:"column(video_id)"` 30 | Order int `orm:"column(order)"` 31 | } -------------------------------------------------------------------------------- /model/video.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | _ "github.com/astaxie/beego/orm" 5 | ) 6 | 7 | type Video struct { 8 | Id int `orm:"column(id)"` 9 | VideoId string `orm:"column(video_id)"` 10 | AuthorId string `orm:"column(author_id)"` 11 | } 12 | 13 | type VideoInfo struct{ 14 | Id int `orm:"column(id)"` 15 | VideoId string `orm:"column(video_id)"` 16 | UrlList string `orm:"column(url_list)"` 17 | Duration int `orm:"column(duration)"` 18 | CommentCount int `orm:"column(comment_count)"` 19 | LikeCount int `orm:"column(like_count)"` 20 | Description string `orm:"column(description)"` 21 | HasDownload int `orm:"column(has_download)"` 22 | VideoPath string `orm:"column(video_path)"` 23 | } 24 | 25 | type VideoTagRef struct{ 26 | Id int `orm:"column(id)"` 27 | VideoId int `orm:"column(video_id)"` 28 | TagId int `orm:"column(tag_id)"` 29 | } 30 | 31 | -------------------------------------------------------------------------------- /service/download.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/astaxie/beego/orm" 7 | "io" 8 | "net/http" 9 | "os" 10 | "project/model" 11 | "project/utils" 12 | ) 13 | 14 | func filter(videoInfo model.VideoInfo) bool{ 15 | return videoInfo.LikeCount > 50000 16 | } 17 | 18 | func DownloadVideo(videoInfos []model.VideoInfo){ 19 | o := orm.NewOrm() 20 | 21 | for _, videoInfo := range videoInfos{ 22 | if !filter(videoInfo){ 23 | continue 24 | } 25 | var videoPath = videoInfo.VideoPath 26 | var urlList []string 27 | err := json.Unmarshal([]byte(videoInfo.UrlList), &urlList) 28 | if err != nil{ 29 | fmt.Println(err) 30 | } 31 | for _, url := range urlList{ 32 | tsVideoPath := utils.Mp4ToTsFileName(videoPath) 33 | for ; ; { 34 | downloadVideoErr := DownloadDo(url, videoPath) 35 | if downloadVideoErr != nil{ 36 | fmt.Println(downloadVideoErr) 37 | }else{ 38 | if utils.CheckVideo(videoPath) == nil && utils.Mp4ToTs(videoPath, tsVideoPath) == nil{ 39 | utils.DeleteFile(videoPath) 40 | break 41 | }else{ 42 | utils.DeleteFile(videoPath) 43 | utils.DeleteFile(tsVideoPath) 44 | } 45 | } 46 | } 47 | videoInfo.HasDownload = 1 48 | videoInfo.VideoPath = tsVideoPath 49 | _, err := o.Update(&videoInfo) 50 | if err != nil { 51 | fmt.Println(err) 52 | } 53 | } 54 | } 55 | } 56 | 57 | func DownloadHttpFile(avatarUrl, videoUrl string, coverUrl string) (string, string, string, error) { 58 | var localAvatar, localCover, localVideo string 59 | localAvatar = "download/avatar/" + utils.Md5(avatarUrl) + ".jpeg" 60 | localVideo = "download/video/" + utils.Md5(videoUrl) + ".mp4" 61 | localCover = "download/cover/" + utils.Md5(coverUrl) + ".jpeg" 62 | err := DownloadDo(avatarUrl, localAvatar) 63 | if err != nil { 64 | return "", "", "", err 65 | } 66 | err = DownloadDo(videoUrl, localVideo) 67 | if err != nil { 68 | return "", "", "", err 69 | } 70 | err = DownloadDo(coverUrl, localCover) 71 | if err != nil { 72 | return "", "", "", err 73 | } 74 | return localAvatar, localCover, localVideo, nil 75 | } 76 | 77 | func DownloadDo(url, saveFile string) error { 78 | client := &http.Client{} 79 | req, err := http.NewRequest("GET", url, nil) 80 | if err != nil{ 81 | fmt.Printf("%s", err) 82 | return nil 83 | } 84 | 85 | req.Header.Add("User-Agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1") 86 | req.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3") 87 | req.Header.Add("Accept-Encoding", "gzip, deflate, br") 88 | req.Header.Add("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8") 89 | res, err := client.Do(req) 90 | if err != nil { 91 | return err 92 | } 93 | defer res.Body.Close() 94 | f, err := os.Create(saveFile) 95 | if err != nil { 96 | _ = os.Remove(saveFile) 97 | return err 98 | } 99 | _, err = io.Copy(f, res.Body) 100 | if err != nil { 101 | _ = os.Remove(saveFile) 102 | return err 103 | } 104 | return nil 105 | } 106 | -------------------------------------------------------------------------------- /service/feed.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "fmt" 5 | "github.com/astaxie/beego/orm" 6 | "project/dao" 7 | "project/model" 8 | "project/utils" 9 | ) 10 | 11 | func HandleFeed(feed model.FeedJson){ 12 | var awemeList = feed.AwemeList 13 | 14 | var videos []model.Video 15 | var videoInfos []model.VideoInfo 16 | for _, aweme := range awemeList { 17 | video := new(model.Video) 18 | videoInfo := new(model.VideoInfo) 19 | aweme.LoadVideo(video) 20 | aweme.LoadVideoInfo(videoInfo) 21 | videoInfo.VideoPath = utils.GenerateVideoPath(videoInfo.VideoId) 22 | videos = append(videos, *video) 23 | videoInfos = append(videoInfos, *videoInfo) 24 | } 25 | o := orm.NewOrm() 26 | _ = o.Begin() 27 | insertVideoErr := dao.InsertVideos(videos, o) 28 | if insertVideoErr != nil{ 29 | fmt.Println(insertVideoErr) 30 | } 31 | insertVideoInfoErr := dao.InsertVideoInfos(videoInfos, o) 32 | if insertVideoInfoErr != nil{ 33 | fmt.Println(insertVideoInfoErr) 34 | } 35 | if insertVideoErr != nil || insertVideoInfoErr != nil { 36 | _ = o.Rollback() 37 | return 38 | } 39 | _ = o.Commit() 40 | 41 | go DownloadVideo(videoInfos) 42 | } -------------------------------------------------------------------------------- /service/hot.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | 4 | -------------------------------------------------------------------------------- /static/client_secret.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | } -------------------------------------------------------------------------------- /static/config_base.toml: -------------------------------------------------------------------------------- 1 | [gin] 2 | addr = 8090 3 | 4 | [app] 5 | packageName = "com.ss.android.ugc.aweme" # 抖音app包名 6 | startPath = ".main.MainActivity" # 抖音app包首页path 7 | restart = 60 # 重新打开时间 单位秒 8 | sleep = 60 # 重新打开睡眠时间 单位秒 9 | 10 | 11 | 12 | [swipe] 13 | startX = 500 14 | startY = 1000 15 | endX = 489 16 | endY = 123 17 | sleep = 3000 # 滑动等待时间 单位毫秒 18 | 19 | 20 | -------------------------------------------------------------------------------- /static/config_darwin.toml: -------------------------------------------------------------------------------- 1 | 2 | [anyproxy] 3 | rule_js_path = "" 4 | 5 | [youtube] 6 | key_path = "" 7 | combine_video_path = '' 8 | 9 | [douyin] 10 | store_path = '' 11 | video_type = ".mp4" 12 | 13 | [database] 14 | driver_name = "mysql" 15 | alias_name = "default" 16 | user_name = "root" 17 | password = "" 18 | ip = "" 19 | port = "" 20 | db_name = "douyin2youtube" 21 | charset = "utf-8" 22 | 23 | -------------------------------------------------------------------------------- /static/config_linux.toml: -------------------------------------------------------------------------------- 1 | [anyproxy] 2 | rule_js_path = "" 3 | 4 | [youtube] 5 | key_path = "" 6 | combine_video_path = '' 7 | 8 | [douyin] 9 | store_path = '' 10 | video_type = "" 11 | 12 | [DataBase] 13 | driver_name = "" 14 | 15 | alias_name = "" 16 | user_name = "root" 17 | password = "" 18 | ip = "127.0.0.1" 19 | port = "3306" 20 | db_name = "douyin2youtube" 21 | charset = "utf-8" -------------------------------------------------------------------------------- /static/create_table.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `tag` ( 2 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 3 | `name` varchar(32) NOT NULL DEFAULT '', 4 | PRIMARY KEY (`id`) 5 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 6 | 7 | 8 | 9 | # Dump of table upload 10 | # ------------------------------------------------------------ 11 | 12 | CREATE TABLE `upload` ( 13 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 14 | `upload_id` varchar(64) NOT NULL DEFAULT '' COMMENT '上传ID', 15 | `upload_result` smallint(6) NOT NULL DEFAULT '1' COMMENT '0:上传成功 1:上传失败', 16 | `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 17 | `modify_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间', 18 | PRIMARY KEY (`id`) 19 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 20 | 21 | 22 | 23 | # Dump of table upload_info 24 | # ------------------------------------------------------------ 25 | 26 | CREATE TABLE `upload_info` ( 27 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 28 | `upload_id` varchar(64) NOT NULL DEFAULT '' COMMENT '上传ID', 29 | `title` varchar(128) NOT NULL DEFAULT '' COMMENT '标题', 30 | `description` text NOT NULL COMMENT '描述', 31 | `category` int(11) NOT NULL DEFAULT '0' COMMENT '类别', 32 | `keywords` varchar(128) NOT NULL DEFAULT '' COMMENT '关键词(json)', 33 | `privacy` varchar(32) NOT NULL DEFAULT '' COMMENT '是否公开', 34 | `video_path` varchar(128) NOT NULL DEFAULT '' COMMENT '文件路径', 35 | `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 36 | `modify_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间', 37 | PRIMARY KEY (`id`) 38 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 39 | 40 | 41 | 42 | # Dump of table upload_video_ref 43 | # ------------------------------------------------------------ 44 | 45 | CREATE TABLE `upload_video_ref` ( 46 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 47 | `upload_id` varchar(64) NOT NULL DEFAULT '' COMMENT '上传id', 48 | `video_id` varchar(64) NOT NULL DEFAULT '' COMMENT '视频id', 49 | `order` smallint(6) NOT NULL DEFAULT '0' COMMENT '视频位次', 50 | `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 51 | `modify_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间', 52 | PRIMARY KEY (`id`) 53 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 54 | 55 | 56 | 57 | # Dump of table video 58 | # ------------------------------------------------------------ 59 | 60 | CREATE TABLE `video` ( 61 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 62 | `video_id` varchar(64) NOT NULL DEFAULT '' COMMENT '视频ID', 63 | `author_id` varchar(32) NOT NULL DEFAULT '' COMMENT '用户id', 64 | `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 65 | `modify_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间', 66 | PRIMARY KEY (`id`) 67 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 68 | 69 | 70 | 71 | # Dump of table video_info 72 | # ------------------------------------------------------------ 73 | 74 | CREATE TABLE `video_info` ( 75 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 76 | `video_id` varchar(64) NOT NULL DEFAULT '' COMMENT '视频ID', 77 | `url_list` text NOT NULL COMMENT '视频地址(json)', 78 | `duration` int(11) NOT NULL DEFAULT '0' COMMENT '持续时间', 79 | `comment_count` int(11) NOT NULL DEFAULT '0' COMMENT '评论数', 80 | `like_count` int(11) NOT NULL DEFAULT '0' COMMENT '点赞数', 81 | `description` text NOT NULL COMMENT '视频描述', 82 | `has_download` smallint(6) NOT NULL DEFAULT '0' COMMENT '0:下载失败, 1:下载成功', 83 | `video_path` varchar(128) NOT NULL DEFAULT '' COMMENT '视频路径', 84 | `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 85 | `modify_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间', 86 | PRIMARY KEY (`id`), 87 | UNIQUE KEY `uniq_video` (`video_id`) 88 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 89 | 90 | 91 | 92 | # Dump of table video_tag_ref 93 | # ------------------------------------------------------------ 94 | 95 | CREATE TABLE `video_tag_ref` ( 96 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 97 | `video_id` varchar(64) NOT NULL DEFAULT '0' COMMENT '视频ID', 98 | `tag_id` int(11) NOT NULL DEFAULT '0' COMMENT '标签ID', 99 | `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 100 | `modify_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间', 101 | PRIMARY KEY (`id`) 102 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 103 | -------------------------------------------------------------------------------- /static/mp4tots.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd ~/Movies/douyin/download/ 3 | filenames=$(ls) 4 | for file in ${filenames};do 5 | to_file=${file/".mp4"/".ts"} 6 | echo $to_file_name 7 | `ffmpeg -i $file -vcodec copy -acodec copy -vbsf h264_mp4toannexb $to_file` 8 | done -------------------------------------------------------------------------------- /static/rule_default.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | 5 | summary: 'the default rule for AnyProxy', 6 | 7 | /** 8 | * @param {object} requestDetail 9 | * @param {string} requestDetail.protocol 10 | * @param {object} requestDetail.requestOptions 11 | * @param {object} requestDetail.requestData 12 | * @param {object} requestDetail.response 13 | * @param {number} requestDetail.response.statusCode 14 | * @param {object} requestDetail.response.header 15 | * @param {buffer} requestDetail.response.body 16 | * @returns 17 | */* beforeSendRequest(requestDetail) { 18 | const localResponse = { 19 | statusCode: 200, 20 | header: { 21 | 'Content-Type': 'image/gif' 22 | }, 23 | body: 'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==' 24 | }; 25 | // 屏蔽app请求的图片数据 加快 app的响应速度 26 | if (/byteimg\.com/i.test(requestDetail.url)) { //图片链接 27 | return { 28 | response: localResponse 29 | } 30 | } 31 | if (/ixigua\.com/i.test(requestDetail.url)) { //视频链接 32 | return { 33 | response: localResponse 34 | } 35 | } 36 | // 屏蔽app请求的视频数据 加快 app的响应速度 37 | // 屏蔽app请求google服务 加快 app的响应速度 38 | if (/google/i.test(requestDetail.url)) { 39 | return { 40 | response: { 41 | statusCode: 200, 42 | header: { 43 | 'Content-Type': 'application/json' 44 | }, 45 | body: '[]' 46 | } 47 | } 48 | } 49 | return null; 50 | }, 51 | 52 | 53 | /** 54 | * @param {object} requestDetail 55 | * @param {object} responseDetail 56 | */* beforeSendResponse(requestDetail, responseDetail) { 57 | // 匹配请求推荐列表的接口请求 58 | if (/aweme\/v1\/feed/i.test(requestDetail.url)) { 59 | var data = responseDetail.response.body.toString(); 60 | //将匹配到的json发送到自己的服务器 61 | HttpPost({json: data}, "/feed/"); 62 | } 63 | return null; 64 | }, 65 | 66 | 67 | /** 68 | * default to return null 69 | * the user MUST return a boolean when they do implement the interface in rule 70 | * 71 | * @param {any} requestDetail 72 | * @returns 73 | */ 74 | 75 | /** 76 | * @param {any} requestDetail 77 | * @param {any} error 78 | * @returns 79 | */* onError(requestDetail, error) { 80 | return null; 81 | }, 82 | 83 | 84 | /** 85 | * @param {any} requestDetail 86 | * @param {any} error 87 | * @returns 88 | */* onConnectError(requestDetail, error) { 89 | return null; 90 | }, 91 | }; 92 | 93 | 94 | //将json发送到服务器,str为json内容,path是接收程序的路径和文件名 95 | function HttpPost(data, path) { 96 | var http_url = 'http://127.0.0.1:8090' + path; 97 | var content = require('querystring').stringify(data); 98 | var parse_u = require('url').parse(http_url, true); 99 | var isHttp = parse_u.protocol == 'http:'; 100 | var options = { 101 | host: parse_u.hostname, 102 | port: parse_u.port || (isHttp ? 80 : 443), 103 | path: parse_u.path, 104 | method: 'POST', 105 | headers: { 106 | 'Content-Type': 'application/x-www-form-urlencoded', 107 | 'Content-Length': content.length 108 | } 109 | }; 110 | var req = require(isHttp ? 'http' : 'https').request(options, function (res) { 111 | var _data = ''; 112 | res.on('data', function (chunk) { 113 | _data += chunk; 114 | }); 115 | res.on('end', function () { 116 | // console.log("\n--->>\nresult:", _data) 117 | }); 118 | }); 119 | req.write(content); 120 | req.end(); 121 | } 122 | -------------------------------------------------------------------------------- /test/go_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import "testing" 4 | 5 | 6 | func TestSliceParam(t *testing.T) { 7 | var a []int 8 | a = append(a, 1) 9 | } 10 | -------------------------------------------------------------------------------- /utils/file.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "project/config" 4 | 5 | func GenerateVideoPath(videoId string) string{ 6 | return config.GetString("douyin.store_path") + videoId + 7 | config.GetString("douyin.video_type") 8 | } 9 | -------------------------------------------------------------------------------- /utils/shell.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | "strings" 7 | ) 8 | 9 | func ExecShellCommand(bin string, commandArgs []string) error{ 10 | cmd := exec.Command(bin, commandArgs...) 11 | cmd.Stderr = os.Stderr 12 | err := cmd.Run() 13 | return err 14 | } 15 | 16 | const ( 17 | mp4Tool = "MP4Box" 18 | ffmpegTool = "ffmpeg" 19 | ) 20 | 21 | func Mp4ToTs(videoPath string, toVideoPath string) error{ 22 | var commandArgs = strings.Split("-i 1.mp4 -vcodec copy -acodec copy -vbsf h264_mp4toannexb 1.ts", " ") 23 | commandArgs[1] = videoPath 24 | commandArgs[len(commandArgs) - 1] = toVideoPath 25 | return ExecShellCommand(ffmpegTool, commandArgs) 26 | } 27 | 28 | func CombineVideosUseMp4(videoPaths []string, toPath string) error{ 29 | var commandArgs []string 30 | for _, videoPath := range videoPaths{ 31 | commandArgs = append(commandArgs, "-cat") 32 | commandArgs = append(commandArgs, videoPath) 33 | } 34 | commandArgs = append(commandArgs, "-new") 35 | commandArgs = append(commandArgs, toPath) 36 | return ExecShellCommand(mp4Tool, commandArgs) 37 | } 38 | 39 | func CombineVideosUseFfmpeg(videoPaths []string, toPath string) error{ 40 | var commandArgs = []string{"-i"} 41 | var contacts = "concat:" 42 | for index, videoPath := range videoPaths{ 43 | contacts += videoPath 44 | if index != len(videoPaths) - 1{ 45 | contacts += "|" 46 | } 47 | } 48 | commandArgs = append(commandArgs, contacts) 49 | commandArgs = append(commandArgs, 50 | strings.Split("-acodec copy -vcodec copy -absf aac_adtstoasc " + toPath , " ")...) 51 | 52 | return ExecShellCommand(ffmpegTool, commandArgs) 53 | } 54 | 55 | func CheckVideo(videoPath string) error{ 56 | checkVideoCommand := "-v error -i " + videoPath + " -f null -" 57 | return ExecShellCommand(ffmpegTool, strings.Split(checkVideoCommand, " ")) 58 | } 59 | 60 | func DeleteFile(filePath string) { 61 | var args = make([]string, 1) 62 | args[0] = filePath 63 | _ = ExecShellCommand("rm", args) 64 | } 65 | -------------------------------------------------------------------------------- /utils/str.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | "hash/crc32" 7 | "strings" 8 | ) 9 | 10 | func Md5(str string) string { 11 | h := md5.New() 12 | h.Write([]byte(str)) 13 | return hex.EncodeToString(h.Sum(nil)) 14 | } 15 | 16 | 17 | func HashCode(s string) int{ 18 | v := int(crc32.ChecksumIEEE([]byte(s))) 19 | if v >= 0 { 20 | return v 21 | } 22 | if -v >= 0 { 23 | return -v 24 | } 25 | // v == MinInt 26 | return 0 27 | } 28 | 29 | func Mp4ToTsFileName(name string) string{ 30 | return strings.Replace(name, ".mp4", ".ts", -1) 31 | } 32 | -------------------------------------------------------------------------------- /youtube/authorize.go: -------------------------------------------------------------------------------- 1 | package youtube 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "net" 9 | "net/http" 10 | "net/url" 11 | "os" 12 | "os/exec" 13 | "os/user" 14 | "path/filepath" 15 | "project/config" 16 | "runtime" 17 | 18 | "golang.org/x/net/context" 19 | "golang.org/x/oauth2" 20 | "golang.org/x/oauth2/google" 21 | ) 22 | 23 | // This variable indicates whether the script should launch a web server to 24 | // initiate the authorization flow or just display the URL in the terminal 25 | // window. Note the following instructions based on this setting: 26 | // * launchWebServer = true 27 | // 1. Use OAuth2 credentials for a web application 28 | // 2. Define authorized redirect URIs for the credential in the Google APIs 29 | // Console and set the RedirectURL property on the static object to one 30 | // of those redirect URIs. For example: 31 | // static.RedirectURL = "http://localhost:8090" 32 | // 3. In the startWebServer function below, update the URL in this line 33 | // to match the redirect URI you selected: 34 | // listener, err := net.Listen("tcp", "localhost:8090") 35 | // The redirect URI identifies the URI to which the user is sent after 36 | // completing the authorization flow. The listener then captures the 37 | // authorization code in the URL and passes it back to this script. 38 | // * launchWebServer = false 39 | // 1. Use OAuth2 credentials for an installed application. (When choosing 40 | // the application type for the OAuth2 client ID, select "Other".) 41 | // 2. Set the redirect URI to "urn:ietf:wg:oauth:2.0:oob", like this: 42 | // static.RedirectURL = "urn:ietf:wg:oauth:2.0:oob" 43 | // 3. When running the script, complete the auth flow. Then copy the 44 | // authorization code from the browser and enter it on the command line. 45 | const launchWebServer = false 46 | 47 | const missingClientSecretsMessage = ` 48 | Please configure OAuth 2.0 49 | To make this sample run, you need to populate the client_secrets.json file 50 | found at: 51 | %v 52 | with information from the {{ Google Cloud Console }}{{ https://cloud.google.com/console }}For more information about the client_secrets.json file format, please visit: 53 | https://developers.google.com/api-client-library/python/guide/aaa_client_secrets 54 | ` 55 | 56 | // getClient uses a Context and Config to retrieve a Token 57 | // then generate a Client. It returns the generated Client. 58 | func getClient(scope string) *http.Client { 59 | ctx := context.Background() 60 | 61 | b, err := ioutil.ReadFile(config.GetString("youtube.key_path")) 62 | if err != nil { 63 | log.Fatalf("Unable to read client secret file: %v", err) 64 | } 65 | 66 | // If modifying the scope, delete your previously saved credentials 67 | // at ~/.credentials/youtube-go.json 68 | config, err := google.ConfigFromJSON(b, scope) 69 | if err != nil { 70 | log.Fatalf("Unable to parse client secret file to static: %v", err) 71 | } 72 | 73 | // Use a redirect URI like this for a web app. The redirect URI must be a 74 | // valid one for your OAuth2 credentials. 75 | config.RedirectURL = "urn:ietf:wg:oauth:2.0:oob" 76 | // Use the following redirect URI if launchWebServer=false in oauth2.go 77 | // static.RedirectURL = "urn:ietf:wg:oauth:2.0:oob" 78 | 79 | cacheFile, err := tokenCacheFile() 80 | if err != nil { 81 | log.Fatalf("Unable to get path to cached credential file. %v", err) 82 | } 83 | tok, err := tokenFromFile(cacheFile) 84 | if err != nil { 85 | authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline) 86 | if launchWebServer { 87 | fmt.Println("Trying to get token from web") 88 | tok, err = getTokenFromWeb(config, authURL) 89 | } else { 90 | fmt.Println("Trying to get token from prompt") 91 | tok, err = getTokenFromPrompt(config, authURL) 92 | } 93 | if err == nil { 94 | saveToken(cacheFile, tok) 95 | } 96 | } 97 | return config.Client(ctx, tok) 98 | } 99 | 100 | // startWebServer starts a web server that listens on http://localhost:8080. 101 | // The webserver waits for an oauth code in the three-legged auth flow. 102 | func startWebServer() (codeCh chan string, err error) { 103 | listener, err := net.Listen("tcp", "localhost:8090") 104 | if err != nil { 105 | return nil, err 106 | } 107 | codeCh = make(chan string) 108 | 109 | go http.Serve(listener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 110 | code := r.FormValue("code") 111 | codeCh <- code // send code to OAuth flow 112 | listener.Close() 113 | w.Header().Set("Content-Type", "text/plain") 114 | fmt.Fprintf(w, "Received code: %v\r\nYou can now safely close this browser window.", code) 115 | })) 116 | 117 | return codeCh, nil 118 | } 119 | 120 | // openURL opens a browser window to the specified location. 121 | // This code originally appeared at: 122 | // http://stackoverflow.com/questions/10377243/how-can-i-launch-a-process-that-is-not-a-file-in-go 123 | func openURL(url string) error { 124 | var err error 125 | switch runtime.GOOS { 126 | case "linux": 127 | err = exec.Command("xdg-open", url).Start() 128 | case "windows": 129 | err = exec.Command("rundll32", "url.dll,FileProtocolHandler", "http://localhost:4001/").Start() 130 | case "darwin": 131 | err = exec.Command("open", url).Start() 132 | default: 133 | err = fmt.Errorf("Cannot open URL %s on this platform", url) 134 | } 135 | return err 136 | } 137 | 138 | // Exchange the authorization code for an access token 139 | func exchangeToken(config *oauth2.Config, code string) (*oauth2.Token, error) { 140 | tok, err := config.Exchange(oauth2.NoContext, code) 141 | if err != nil { 142 | log.Fatalf("Unable to retrieve token %v", err) 143 | } 144 | return tok, nil 145 | } 146 | 147 | // getTokenFromPrompt uses Config to request a Token and prompts the user 148 | // to enter the token on the command line. It returns the retrieved Token. 149 | func getTokenFromPrompt(config *oauth2.Config, authURL string) (*oauth2.Token, error) { 150 | var code string 151 | fmt.Printf("Go to the following link in your browser. After completing " + 152 | "the authorization flow, enter the authorization code on the command " + 153 | "line: \n%v\n", authURL) 154 | 155 | if _, err := fmt.Scan(&code); err != nil { 156 | log.Fatalf("Unable to read authorization code %v", err) 157 | } 158 | fmt.Println(authURL) 159 | return exchangeToken(config, code) 160 | } 161 | 162 | // getTokenFromWeb uses Config to request a Token. 163 | // It returns the retrieved Token. 164 | func getTokenFromWeb(config *oauth2.Config, authURL string) (*oauth2.Token, error) { 165 | codeCh, err := startWebServer() 166 | if err != nil { 167 | fmt.Printf("Unable to start a web server.") 168 | return nil, err 169 | } 170 | 171 | err = openURL(authURL) 172 | if err != nil { 173 | log.Fatalf("Unable to open authorization URL in web server: %v", err) 174 | } else { 175 | fmt.Println("Your browser has been opened to an authorization URL.", 176 | " This program will resume once authorization has been provided.\n") 177 | fmt.Println(authURL) 178 | } 179 | 180 | // Wait for the web server to get the code. 181 | code := <-codeCh 182 | return exchangeToken(config, code) 183 | } 184 | 185 | // tokenCacheFile generates credential file path/filename. 186 | // It returns the generated credential path/filename. 187 | func tokenCacheFile() (string, error) { 188 | usr, err := user.Current() 189 | if err != nil { 190 | return "", err 191 | } 192 | tokenCacheDir := filepath.Join(usr.HomeDir, ".credentials") 193 | os.MkdirAll(tokenCacheDir, 0700) 194 | return filepath.Join(tokenCacheDir, 195 | url.QueryEscape("youtube-go.json")), err 196 | } 197 | 198 | // tokenFromFile retrieves a Token from a given file path. 199 | // It returns the retrieved Token and any read error encountered. 200 | func tokenFromFile(file string) (*oauth2.Token, error) { 201 | f, err := os.Open(file) 202 | if err != nil { 203 | return nil, err 204 | } 205 | t := &oauth2.Token{} 206 | err = json.NewDecoder(f).Decode(t) 207 | defer f.Close() 208 | return t, err 209 | } 210 | 211 | // saveToken uses a file path to create a file and store the 212 | // token in it. 213 | func saveToken(file string, token *oauth2.Token) { 214 | fmt.Println("trying to save token") 215 | fmt.Printf("Saving credential file to: %s\n", file) 216 | f, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) 217 | if err != nil { 218 | log.Fatalf("Unable to cache oauth token: %v", err) 219 | } 220 | defer f.Close() 221 | json.NewEncoder(f).Encode(token) 222 | } 223 | -------------------------------------------------------------------------------- /youtube/cronjob.go: -------------------------------------------------------------------------------- 1 | package youtube 2 | 3 | import ( 4 | "fmt" 5 | "github.com/astaxie/beego/orm" 6 | "project/config" 7 | "project/dao" 8 | _ "project/dao" 9 | "project/model" 10 | "project/utils" 11 | "strconv" 12 | "time" 13 | ) 14 | 15 | func Task(){ 16 | o := orm.NewOrm() 17 | var source = new(Source) 18 | var videoInfos []model.VideoInfo 19 | var random RandomChoose 20 | var uploadInfo model.UploadInfo 21 | 22 | source.videoInfos = &videoInfos 23 | 24 | filterVideos(o, &videoInfos) 25 | 26 | chooseVideoInfos := random.choose(source) 27 | var videoPaths = make([]string, len(chooseVideoInfos)) 28 | for index, videoInfo := range chooseVideoInfos{ 29 | videoPaths[index] = videoInfo.VideoPath 30 | } 31 | uploadVideoId := generateUploadId(&chooseVideoInfos) 32 | 33 | var toPath = config.GetString("youtube.combine_video_path") + 34 | uploadVideoId + config.GetString("douyin.video_type") 35 | 36 | err := utils.CombineVideosUseFfmpeg(videoPaths, toPath) 37 | if err != nil{ 38 | panic(err) 39 | } 40 | uploadInfo.UploadId = uploadVideoId 41 | uploadInfo.Description = "[TikTok]" + chooseVideoInfos[0].Description 42 | uploadInfo.Title = "欢迎关注,每日抖音热搜,要你好看,一起抖不停。" 43 | uploadInfo.Category = "10" 44 | uploadInfo.Keywords = "抖音,TikTok,Musicly" 45 | uploadInfo.Privacy = "public" 46 | uploadInfo.VideoPath = toPath 47 | 48 | writeDataBase(chooseVideoInfos, uploadInfo, o) 49 | 50 | //Upload(uploadInfo) 51 | } 52 | 53 | 54 | func generateUploadId(videoInfos *[]model.VideoInfo) string{ 55 | nowTime := time.Now() 56 | date := strconv.Itoa(nowTime.Year()) + "_" + 57 | strconv.Itoa(int(nowTime.Month())) + "_" + strconv.Itoa(nowTime.Day()) 58 | 59 | var sumVideoIds = "" 60 | for _, videoInfo := range *videoInfos{ 61 | sumVideoIds += videoInfo.VideoId 62 | } 63 | 64 | hashcode := utils.HashCode(sumVideoIds) 65 | return date + "_" + strconv.Itoa(hashcode) 66 | } 67 | 68 | func filterVideos(o orm.Ormer, videoInfos *[]model.VideoInfo){ 69 | uploadedVideoIds := dao.QueryUploadedVideo(o) 70 | filteredVideoInfos := dao.QueryNotUploadVideo(uploadedVideoIds, o) 71 | *videoInfos = filteredVideoInfos 72 | } 73 | 74 | func writeDataBase(chooseVideoInfos []model.VideoInfo, uploadInfo model.UploadInfo, o orm.Ormer) { 75 | var uploadVideoRefs []model.UploadVideoRef 76 | uploadVideoRefs = make([]model.UploadVideoRef, len(chooseVideoInfos)) 77 | for index, videoInfo := range chooseVideoInfos{ 78 | var uploadVideoRef model.UploadVideoRef 79 | uploadVideoRef.UploadId = uploadInfo.UploadId 80 | uploadVideoRef.VideoId = videoInfo.VideoId 81 | uploadVideoRef.Order = index 82 | uploadVideoRefs[index] = uploadVideoRef 83 | } 84 | _, err := o.Insert(&uploadInfo) 85 | if err != nil{ 86 | fmt.Println(err) 87 | } 88 | err = dao.InsertUploadVideoRefs(uploadVideoRefs, o) 89 | if err != nil{ 90 | fmt.Println(err) 91 | } 92 | } -------------------------------------------------------------------------------- /youtube/strategy.go: -------------------------------------------------------------------------------- 1 | package youtube 2 | 3 | import ( 4 | "math/rand" 5 | "project/model" 6 | "time" 7 | ) 8 | 9 | type Source struct{ 10 | videoInfos *[]model.VideoInfo 11 | } 12 | type StrategyType int32 13 | const ( 14 | randomStrategy StrategyType = 1 15 | videoDuration int = 10 * 60 * 1000 // unit ms 16 | ) 17 | 18 | // 策略模式 19 | type chooseVideo interface { 20 | choose(source *Source) []model.VideoInfo 21 | } 22 | var strategyMap map[StrategyType]chooseVideo 23 | 24 | // 随机算法 25 | type RandomChoose struct{ 26 | 27 | } 28 | func (random RandomChoose) choose(source *Source) ([]model.VideoInfo){ 29 | var durationSum int 30 | var length = 0 31 | videoInfos := source.videoInfos 32 | rand.Seed(time.Now().Unix()) 33 | for durationSum = 0; durationSum < videoDuration; length += 1{ 34 | if length >= len(*videoInfos){ 35 | break 36 | } 37 | index := rand.Intn(len(*videoInfos) - length) 38 | toIndex := len(*videoInfos) - length - 1 39 | 40 | durationSum += (*videoInfos)[index].Duration 41 | 42 | temp := (*videoInfos)[toIndex] 43 | (*videoInfos)[toIndex] = (*videoInfos)[index] 44 | (*videoInfos)[index] = temp 45 | } 46 | 47 | return (*videoInfos)[len(*videoInfos) - length:] 48 | } 49 | 50 | 51 | func init(){ 52 | strategyMap = make(map[StrategyType]chooseVideo) 53 | strategyMap[randomStrategy] = RandomChoose{} 54 | } 55 | -------------------------------------------------------------------------------- /youtube/upload.go: -------------------------------------------------------------------------------- 1 | package youtube 2 | 3 | 4 | import ( 5 | "fmt" 6 | "google.golang.org/api/youtube/v3" 7 | "log" 8 | "os" 9 | "project/model" 10 | "strings" 11 | ) 12 | 13 | 14 | func Upload(uploadInfo model.UploadInfo) error{ 15 | if uploadInfo.VideoPath == "" { 16 | log.Fatalf("You must provide a filename of a video file to upload") 17 | } 18 | 19 | client := getClient(youtube.YoutubeUploadScope) 20 | 21 | service, err := youtube.New(client) 22 | if err != nil { 23 | log.Fatalf("Error creating YouTube client: %v", err) 24 | } 25 | 26 | upload := &youtube.Video{ 27 | Snippet: &youtube.VideoSnippet{ 28 | Title: uploadInfo.Title, 29 | Description: uploadInfo.Description, 30 | CategoryId: uploadInfo.Category, 31 | }, 32 | Status: &youtube.VideoStatus{PrivacyStatus: uploadInfo.Privacy}, 33 | } 34 | 35 | // The API returns a 400 Bad Request response if tags is an empty string. 36 | if strings.Trim(uploadInfo.Keywords, "") != "" { 37 | upload.Snippet.Tags = strings.Split(uploadInfo.Keywords, ",") 38 | } 39 | 40 | call := service.Videos.Insert("snippet,status", upload) 41 | 42 | file, err := os.Open(uploadInfo.VideoPath) 43 | defer file.Close() 44 | if err != nil { 45 | log.Fatalf("Error opening %v: %v", uploadInfo.VideoPath, err) 46 | } 47 | 48 | response, err := call.Media(file).Do() 49 | if err == nil{ 50 | fmt.Printf("Upload successful! Video ID: %v\n", response.Id) 51 | } 52 | return err 53 | } 54 | 55 | func handleError(err error){ 56 | fmt.Printf("%s", err) 57 | } 58 | --------------------------------------------------------------------------------