mirror of
https://github.com/pikami/tiktok-dl.git
synced 2024-11-25 09:15:41 +00:00
Added ability to download single videos
This commit is contained in:
parent
38c23fd9f5
commit
39cfeb40fb
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,3 +1,5 @@
|
|||||||
|
.vscode
|
||||||
|
__debug_bin
|
||||||
downloads
|
downloads
|
||||||
*.exe
|
*.exe
|
||||||
tiktok-dl
|
tiktok-dl
|
17
README.md
Normal file
17
README.md
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# TikTok-DL
|
||||||
|
|
||||||
|
[![Go Report Card](https://goreportcard.com/badge/github.com/pikami/tiktok-dl)](https://goreportcard.com/report/github.com/pikami/tiktok-dl)
|
||||||
|
|
||||||
|
A simple tiktok video downloader written in go
|
||||||
|
|
||||||
|
## Basic usage
|
||||||
|
Clone this repository and run `go build` to build the executable.\
|
||||||
|
You can download all videos from user by running `./tiktok-dl [Options] TIKTOK_USERNAME`\
|
||||||
|
You can download single video by running `./tiktok-dl [Options] VIDEO_URL`
|
||||||
|
|
||||||
|
## Available options
|
||||||
|
* `-debug` - enables debug mode
|
||||||
|
* `-output some_directory` - Output path (default "./downloads")
|
||||||
|
|
||||||
|
## Acknowledgments
|
||||||
|
This software uses the chromedp for web scraping, it can be found here: https://github.com/chromedp/chromedp
|
@ -23,7 +23,7 @@ func GetUserUploads(username string) []models.Upload {
|
|||||||
opts := append(chromedp.DefaultExecAllocatorOptions[:],
|
opts := append(chromedp.DefaultExecAllocatorOptions[:],
|
||||||
chromedp.DisableGPU,
|
chromedp.DisableGPU,
|
||||||
chromedp.UserDataDir(dir),
|
chromedp.UserDataDir(dir),
|
||||||
chromedp.Flag("headless", models.Config.UserName),
|
chromedp.Flag("headless", !models.Config.Debug),
|
||||||
)
|
)
|
||||||
|
|
||||||
allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...)
|
allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...)
|
||||||
@ -43,7 +43,8 @@ func GetUserUploads(username string) []models.Upload {
|
|||||||
// Navigate to user's page
|
// Navigate to user's page
|
||||||
chromedp.Navigate(`https://www.tiktok.com/@`+username),
|
chromedp.Navigate(`https://www.tiktok.com/@`+username),
|
||||||
// Execute url grabber script
|
// Execute url grabber script
|
||||||
chromedp.EvaluateAsDevTools(utils.ReadFileAsString("getVidLinks.js"), &jsOutput),
|
chromedp.EvaluateAsDevTools(utils.ReadFileAsString("scraper.js"), &jsOutput),
|
||||||
|
chromedp.EvaluateAsDevTools("bootstrapIteratingVideos()", &jsOutput),
|
||||||
// Wait until custom js finishes
|
// Wait until custom js finishes
|
||||||
chromedp.WaitVisible(`video_urls`),
|
chromedp.WaitVisible(`video_urls`),
|
||||||
// Grab url links from our element
|
// Grab url links from our element
|
||||||
|
58
client/getVideoDetails.go
Normal file
58
client/getVideoDetails.go
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/chromedp/chromedp"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
models "../models"
|
||||||
|
utils "../utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetVideoDetails - returns details of video
|
||||||
|
func GetVideoDetails(videoURL string) models.Upload {
|
||||||
|
dir, err := ioutil.TempDir("", "chromedp-example")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(dir)
|
||||||
|
|
||||||
|
opts := append(chromedp.DefaultExecAllocatorOptions[:],
|
||||||
|
chromedp.DisableGPU,
|
||||||
|
chromedp.UserDataDir(dir),
|
||||||
|
chromedp.Flag("headless", !models.Config.Debug),
|
||||||
|
)
|
||||||
|
|
||||||
|
allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
ctx, cancel := chromedp.NewContext(
|
||||||
|
allocCtx,
|
||||||
|
chromedp.WithLogf(log.Printf),
|
||||||
|
)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
ctx, cancel = context.WithTimeout(ctx, 1500*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
var jsOutput string
|
||||||
|
err = chromedp.Run(ctx,
|
||||||
|
// Navigate to user's page
|
||||||
|
chromedp.Navigate(videoURL),
|
||||||
|
// Execute url grabber script
|
||||||
|
chromedp.EvaluateAsDevTools(utils.ReadFileAsString("scraper.js"), &jsOutput),
|
||||||
|
chromedp.EvaluateAsDevTools("bootstrapGetCurrentVideo()", &jsOutput),
|
||||||
|
// Wait until custom js finishes
|
||||||
|
chromedp.WaitVisible(`video_urls`),
|
||||||
|
// Grab url links from our element
|
||||||
|
chromedp.InnerHTML(`video_urls`, &jsOutput),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return models.ParseUpload(jsOutput)
|
||||||
|
}
|
48
main.go
48
main.go
@ -5,27 +5,53 @@ import (
|
|||||||
models "./models"
|
models "./models"
|
||||||
utils "./utils"
|
utils "./utils"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
models.GetConfig()
|
models.GetConfig()
|
||||||
|
url := models.Config.URL
|
||||||
|
|
||||||
username := models.Config.UserName
|
// Single video
|
||||||
|
match, _ := regexp.MatchString("\\/@.+\\/video\\/[0-9]+", url)
|
||||||
|
if match {
|
||||||
|
getUsernameFromVidURLRegex, _ := regexp.Compile("com\\/@.*")
|
||||||
|
parts := strings.Split(getUsernameFromVidURLRegex.FindString(url), "/")
|
||||||
|
username := parts[1][1:]
|
||||||
|
upload := client.GetVideoDetails(url)
|
||||||
|
downloadDir := fmt.Sprintf("%s/%s", models.Config.OutputPath, username)
|
||||||
|
|
||||||
|
utils.InitOutputDirectory(downloadDir)
|
||||||
|
downloadVideo(upload, downloadDir)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tiktok user
|
||||||
|
downloadUser()
|
||||||
|
}
|
||||||
|
|
||||||
|
func downloadVideo(upload models.Upload, downloadDir string) {
|
||||||
|
uploadID := upload.GetUploadID()
|
||||||
|
downloadPath := fmt.Sprintf("%s/%s.mp4", downloadDir, uploadID)
|
||||||
|
|
||||||
|
if utils.CheckIfExists(downloadPath) {
|
||||||
|
fmt.Println("Upload '" + uploadID + "' already downloaded, skipping")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Downloading upload item '" + uploadID + "' to " + downloadPath)
|
||||||
|
utils.DownloadFile(downloadPath, upload.URL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func downloadUser() {
|
||||||
|
username := models.Config.URL
|
||||||
downloadDir := fmt.Sprintf("%s/%s", models.Config.OutputPath, username)
|
downloadDir := fmt.Sprintf("%s/%s", models.Config.OutputPath, username)
|
||||||
uploads := client.GetUserUploads(username)
|
uploads := client.GetUserUploads(username)
|
||||||
|
|
||||||
utils.InitOutputDirectory(downloadDir)
|
utils.InitOutputDirectory(downloadDir)
|
||||||
|
|
||||||
for _, upload := range uploads {
|
for _, upload := range uploads {
|
||||||
uploadID := upload.GetUploadID()
|
downloadVideo(upload, downloadDir)
|
||||||
downloadPath := fmt.Sprintf("%s/%s.mp4", downloadDir, uploadID)
|
|
||||||
|
|
||||||
if utils.CheckIfExists(downloadPath) {
|
|
||||||
fmt.Println("Upload '" + uploadID + "' already downloaded, skipping")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("Downloading upload item '" + uploadID + "' to " + downloadPath)
|
|
||||||
utils.DownloadFile(downloadPath, upload.URL)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
// Config - Runtime configuration
|
// Config - Runtime configuration
|
||||||
var Config struct {
|
var Config struct {
|
||||||
UserName string
|
URL string
|
||||||
OutputPath string
|
OutputPath string
|
||||||
Debug bool
|
Debug bool
|
||||||
}
|
}
|
||||||
@ -21,11 +21,11 @@ func GetConfig() {
|
|||||||
|
|
||||||
args := flag.Args()
|
args := flag.Args()
|
||||||
if len(args) < 1 {
|
if len(args) < 1 {
|
||||||
fmt.Println("Usage: tiktok-dl [OPTIONS] TIKTOK_USERNAME")
|
fmt.Println("Usage: tiktok-dl [OPTIONS] TIKTOK_USERNAME|TIKTOK_URL")
|
||||||
os.Exit(2)
|
os.Exit(2)
|
||||||
}
|
}
|
||||||
|
|
||||||
Config.UserName = flag.Args()[len(args)-1]
|
Config.URL = flag.Args()[len(args)-1]
|
||||||
Config.OutputPath = *outputPath
|
Config.OutputPath = *outputPath
|
||||||
Config.Debug = *debug
|
Config.Debug = *debug
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,13 @@ func ParseUploads(str string) []Upload {
|
|||||||
return uploads
|
return uploads
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ParseUpload - Parses json uploads array
|
||||||
|
func ParseUpload(str string) Upload {
|
||||||
|
var upload Upload
|
||||||
|
json.Unmarshal([]byte(str), &upload)
|
||||||
|
return upload
|
||||||
|
}
|
||||||
|
|
||||||
// GetUploadID - Returns upload id
|
// GetUploadID - Returns upload id
|
||||||
func (u Upload) GetUploadID() string {
|
func (u Upload) GetUploadID() string {
|
||||||
parts := strings.Split(u.ShareLink, "/")
|
parts := strings.Split(u.ShareLink, "/")
|
||||||
|
@ -5,6 +5,8 @@ optStrings = {
|
|||||||
modalClose: '.video-card-modal > div > div.close',
|
modalClose: '.video-card-modal > div > div.close',
|
||||||
modalPlayer: 'div > div > main > div.video-card-modal > div > div.video-card-big > div.video-card-container > div > div > video',
|
modalPlayer: 'div > div > main > div.video-card-modal > div > div.video-card-big > div.video-card-container > div > div > video',
|
||||||
modalShareInput: '.copy-link-container > input',
|
modalShareInput: '.copy-link-container > input',
|
||||||
|
videoPlayer: 'div.video-card-container > div > div > video',
|
||||||
|
videoShareInput: 'div.content-container.border > div.copy-link-container > input',
|
||||||
},
|
},
|
||||||
classes: {
|
classes: {
|
||||||
modalCloseDisabled: 'disabled',
|
modalCloseDisabled: 'disabled',
|
||||||
@ -18,9 +20,9 @@ optStrings = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
createVidUrlElement = function(videoArray) {
|
createVidUrlElement = function(outputObj) {
|
||||||
var urlSetElement = document.createElement(optStrings.tags.resultTag);
|
var urlSetElement = document.createElement(optStrings.tags.resultTag);
|
||||||
urlSetElement.innerText = JSON.stringify(videoArray);
|
urlSetElement.innerText = JSON.stringify(outputObj);
|
||||||
document.getElementsByTagName(optStrings.tags.resultParentTag)[0].appendChild(urlSetElement);
|
document.getElementsByTagName(optStrings.tags.resultParentTag)[0].appendChild(urlSetElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,6 +56,17 @@ getCurrentModalVideo = function() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCurrentVideo = function() {
|
||||||
|
var player = document.querySelector(optStrings.selectors.videoPlayer);
|
||||||
|
var vidUrl = player.getAttribute(optStrings.attributes.src);
|
||||||
|
var shareLink = document.querySelector(optStrings.selectors.videoShareInput).value;
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: vidUrl,
|
||||||
|
shareLink: shareLink
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
scrollWhileNew = function(finishCallback) {
|
scrollWhileNew = function(finishCallback) {
|
||||||
var state = { count: 0 };
|
var state = { count: 0 };
|
||||||
var intervalID = window.setInterval(x => {
|
var intervalID = window.setInterval(x => {
|
||||||
@ -76,13 +89,19 @@ bootstrapIteratingVideos = function() {
|
|||||||
window.clearInterval(intervalID);
|
window.clearInterval(intervalID);
|
||||||
}
|
}
|
||||||
}, 500);
|
}, 500);
|
||||||
|
return 'bootstrapIteratingVideos';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
bootstrapGetCurrentVideo = function() {
|
||||||
|
var video = getCurrentVideo();
|
||||||
|
createVidUrlElement(video);
|
||||||
|
return 'bootstrapGetCurrentVideo';
|
||||||
|
}
|
||||||
|
|
||||||
init = () => {
|
init = () => {
|
||||||
const newProto = navigator.__proto__;
|
const newProto = navigator.__proto__;
|
||||||
delete newProto.webdriver;
|
delete newProto.webdriver;
|
||||||
navigator.__proto__ = newProto;
|
navigator.__proto__ = newProto;
|
||||||
bootstrapIteratingVideos();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
init();
|
init();
|
Loading…
Reference in New Issue
Block a user