25 Commits
v1.1 ... 1.9.1

Author SHA1 Message Date
Pijus Kamandulis
92006d864f TTDL-20 Wait for error/items beefore quiting 2020-04-08 23:26:50 +03:00
Pijus Kamandulis
668b050dee Added sonar-scaner
Create LICENSE

Update go.yml
2020-04-04 21:09:41 +03:00
Pijus Kamandulis
feee0a9154 Added support for vm.tiktok.com urls 2020-03-22 12:38:08 +02:00
Pijus Kamandulis
af7972685e Fixed circular dependency issue 2020-03-22 00:22:08 +02:00
Pijus Kamandulis
f9d35e3bf2 TTDL-7 Added flag; Code clean up 2020-03-22 02:10:24 +02:00
Pijus Kamandulis
9a65746fd4 Update go.yml 2020-02-25 21:44:43 +02:00
Pijus Kamandulis
70c605a696 Merge pull request #6 from intracomof/master
Download videos by hashtag; limit option; get just json data
2020-02-25 21:33:57 +02:00
alexpin
208bffb846 error handling 2020-02-25 21:16:57 +02:00
alexpin
7b9b7688a1 formatter 2020-02-25 21:03:06 +02:00
intracomof
e77c904f89 Merge branch 'master' into master 2020-02-25 21:01:43 +02:00
alexpin
68612282ee default limit value updated; WaitReady(video) removed 2020-02-25 20:55:56 +02:00
Pijus Kamandulis
7a691ad32d TTDL-5 Added better error handling 2020-02-25 20:12:01 +02:00
alexpin
b6bb470064 formatter 2020-02-25 01:01:10 +02:00
alexpin
f724f0f2a2 Download videos by hashtag; get json data without video downloading; limit option 2020-02-25 00:56:19 +02:00
Pijus Kamandulis
1b3f985f42 Improved status output
Added `-quiet` flag

Move out error messages to separate file
2020-02-08 02:52:26 +02:00
Pijus Kamandulis
673bbe1340 TTDL-3 Add deadline flag 2020-01-30 19:05:33 +02:00
Pijus Kamandulis
2af96e899e Fix issue with batch files 2020-01-27 20:46:10 +02:00
Pijus Kamandulis
18c745aaba Fix failing unit tests 2020-01-26 20:11:27 +02:00
Pijus Kamandulis
cd2f2d818b Merge branch 'master' of https://github.com/pikami/tiktok-dl 2020-01-26 20:03:18 +02:00
Pijus Kamandulis
884f9040db Added option to download items by music 2020-01-26 19:45:58 +02:00
Pijus Kamandulis
672bacd3dd Update README.md 2020-01-24 19:14:52 +02:00
Pijus Kamandulis
6f8ab8a277 Added option to download items from list 2020-01-24 19:02:50 +02:00
Pijus Kamandulis
1782a2f12b Added more unit tests 2020-01-22 01:06:35 +02:00
Pijus Kamandulis
6e0e39ada2 Improved parameter parsing 2020-01-21 23:46:30 +02:00
Pijus Kamandulis
320e044f3c Update README.md 2020-01-20 21:29:42 +02:00
38 changed files with 1109 additions and 259 deletions

View File

@@ -1,5 +1,5 @@
name: tiktok-dl_CI
on: [push]
on: [push, pull_request]
jobs:
build:
strategy:
@@ -33,6 +33,9 @@ jobs:
- name: Build
run: npm run build:dist
- name: Copy license
run: cp LICENSE out
- name: Upload Unix Artifacts
if: startsWith(matrix.os, 'ubuntu-')

3
.gitignore vendored
View File

@@ -4,3 +4,6 @@ __debug_bin
downloads
*.exe
tiktok-dl
batch_file.txt
debug.log
.scannerwork

View File

@@ -0,0 +1,12 @@
sonar.organization=pikami
sonar.projectKey=tiktok-dl
sonar.host.url=https://sonarcloud.io
sonar.sources=.
sonar.exclusions=**/*_test.go,**/node_modules/**
sonar.tests=.
sonar.test.inclusions=**/*_test.go
sonar.test.exclusions=**/node_modules/**
sonar.go.coverage.reportPaths=cov.out

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 pikami
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -8,14 +8,24 @@ A simple tiktok video downloader written in go
## Basic usage
Download the executable from `https://github.com/pikami/tiktok-dl/releases`\
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`
You can download single video by running `./tiktok-dl [Options] VIDEO_URL`\
You can download all videos by music by running `./tiktok-dl [Options] MUSIC_URL`\
You can download items listed in a text file by running `./tiktok-dl [OPTIONS] -batch-file path/to/items.txt`
## Build instructions
Clone this repository and run `go build` to build the executable.
## Available options
* `-archive` - Download only videos not listed in the archive file. Record the IDs of all downloaded videos in it.
* `-batch-file` - File containing URLs/Usernames to download, one value per line. Lines starting with '#', are considered as comments and ignored.
* `-deadline` - Sets the timout for scraper logic in seconds (used as a workaround for context deadline exceeded error) (default 1500)
* `-debug` - enables debug mode
* `-json` - Returns whole data, that was scraped from TikTok, in json
* `-limit` - Sets the max count of video that will be downloaded (default infinity)
* `-metadata` - Write video metadata to a .json file
* `-output some_directory` - Output path (default "./downloads")
* `-quiet` - Supress output
## Acknowledgments
This software uses the chromedp for web scraping, it can be found here: https://github.com/chromedp/chromedp
This software uses the **chromedp** for web scraping, it can be found here: https://github.com/chromedp/chromedp \
For releases the JS code is minified by using **terser** toolkit, it can be found here: https://github.com/terser/terser

View File

@@ -0,0 +1,115 @@
package client
import (
"context"
"errors"
"io/ioutil"
"os"
"strings"
"time"
"github.com/chromedp/chromedp"
config "../models/config"
utils "../utils"
log "../utils/log"
)
// GetMusicUploads - Get all uploads by given music
func executeClientAction(url string, jsAction string) (string, error) {
dir, err := ioutil.TempDir("", "chromedp-example")
if err != nil {
return "", err
}
defer os.RemoveAll(dir)
opts := append(chromedp.DefaultExecAllocatorOptions[:],
chromedp.DisableGPU,
chromedp.UserDataDir(dir),
chromedp.Flag("headless", !config.Config.Debug),
)
allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...)
defer cancel()
ctx, cancel := chromedp.NewContext(
allocCtx,
chromedp.WithLogf(log.Logf),
)
defer cancel()
ctx, cancel = context.WithTimeout(ctx, time.Duration(config.Config.Deadline)*time.Second)
defer cancel()
jsOutput, err := runScrapeWithInfo(ctx, jsAction, url)
if strings.HasPrefix(jsOutput, "\"ERR:") {
err = errors.New(jsOutput)
}
return jsOutput, err
}
func runScrapeQuiet(ctx context.Context, jsAction string, url string) (string, error) {
var jsOutput string
if err := chromedp.Run(ctx,
// Navigate to user's page
chromedp.Navigate(url),
// Execute url grabber script
chromedp.EvaluateAsDevTools(utils.ReadFileAsString("scraper.js"), &jsOutput),
chromedp.EvaluateAsDevTools(jsAction, &jsOutput),
// Wait until custom js finishes
chromedp.WaitVisible(`video_urls`),
// Grab url links from our element
chromedp.InnerHTML(`video_urls`, &jsOutput),
); err != nil {
return "", err
}
return jsOutput, nil
}
func runScrapeWithInfo(ctx context.Context, jsAction string, url string) (string, error) {
var jsOutput string
if err := chromedp.Run(ctx,
// Navigate to user's page
chromedp.Navigate(url),
// Execute url grabber script
chromedp.EvaluateAsDevTools(utils.ReadFileAsString("scraper.js"), &jsOutput),
chromedp.EvaluateAsDevTools(jsAction, &jsOutput),
); err != nil {
return "", err
}
for {
if err := chromedp.Run(ctx, chromedp.EvaluateAsDevTools("currentState.preloadCount.toString()", &jsOutput)); err != nil {
return "", err
}
if jsOutput != "0" {
log.Logf("\rPreloading... %s items have been found.", jsOutput)
} else {
log.Logf("\rPreloading...")
}
if err := chromedp.Run(ctx, chromedp.EvaluateAsDevTools("currentState.finished.toString()", &jsOutput)); err != nil {
return "", err
}
if jsOutput == "true" {
break
}
time.Sleep(50 * time.Millisecond)
}
log.Log("\nRetrieving items...")
if err := chromedp.Run(ctx,
// Wait until custom js finishes
chromedp.WaitVisible(`video_urls`),
// Grab url links from our element
chromedp.InnerHTML(`video_urls`, &jsOutput),
); err != nil {
return "", err
}
return jsOutput, nil
}

View File

@@ -0,0 +1,28 @@
package client
import (
"fmt"
models "../models"
config "../models/config"
)
// GetHashtagUploads - Get all uploads marked with given hashtag
func GetHashtagUploads(hashtagURL string) ([]models.Upload, error) {
actionOutput, err := GetHashtagUploadsJSON(hashtagURL)
if err != nil {
return nil, err
}
return models.ParseUploads(actionOutput), nil
}
// GetHashtagUploadsJSON - Get hashtag uploads scrape
func GetHashtagUploadsJSON(hashtagURL string) (string, error) {
jsMethod := fmt.Sprintf("bootstrapIteratingVideos(%d)", config.Config.Limit)
actionOutput, err := executeClientAction(hashtagURL, jsMethod)
if err != nil {
return "", err
}
return actionOutput, nil
}

27
client/getMusicUploads.go Normal file
View File

@@ -0,0 +1,27 @@
package client
import (
"fmt"
models "../models"
config "../models/config"
)
// GetMusicUploads - Get all uploads by given music
func GetMusicUploads(url string) ([]models.Upload, error) {
actionOutput, err := GetMusicUploadsJSON(url)
if err != nil {
return nil, err
}
return models.ParseUploads(actionOutput), nil
}
// GetMusicUploadsJSON - Get music uploads scrape
func GetMusicUploadsJSON(url string) (string, error) {
jsMethod := fmt.Sprintf("bootstrapIteratingVideos(%d)", config.Config.Limit)
actionOutput, err := executeClientAction(url, jsMethod)
if err != nil {
return "", err
}
return actionOutput, nil
}

52
client/getRedirectUrl.go Normal file
View File

@@ -0,0 +1,52 @@
package client
import (
"context"
"github.com/chromedp/chromedp"
"io/ioutil"
"os"
"time"
config "../models/config"
log "../utils/log"
)
func GetRedirectUrl(url string) (string, error) {
dir, err := ioutil.TempDir("", "chromedp-example")
if err != nil {
return "", err
}
defer os.RemoveAll(dir)
opts := append(chromedp.DefaultExecAllocatorOptions[:],
chromedp.DisableGPU,
chromedp.UserDataDir(dir),
chromedp.Flag("headless", !config.Config.Debug),
)
allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...)
defer cancel()
ctx, cancel := chromedp.NewContext(
allocCtx,
chromedp.WithLogf(log.Logf),
)
defer cancel()
ctx, cancel = context.WithTimeout(ctx, time.Duration(config.Config.Deadline)*time.Second)
defer cancel()
var jsOutput string
if err := chromedp.Run(ctx,
// Navigate to user's page
chromedp.Navigate(url),
// Wait until page loads
chromedp.WaitReady(`div`),
// Grab url links from our element
chromedp.EvaluateAsDevTools(`window.location.href`, &jsOutput),
); err != nil {
return "", err
}
return jsOutput, err
}

View File

@@ -1,58 +1,27 @@
package client
import (
"context"
"github.com/chromedp/chromedp"
"io/ioutil"
"log"
"os"
"time"
"fmt"
models "../models"
utils "../utils"
config "../models/config"
)
// GetUserUploads - Get all uploads by user
func GetUserUploads(username string) []models.Upload {
dir, err := ioutil.TempDir("", "chromedp-example")
func GetUserUploads(username string) ([]models.Upload, error) {
actionOutput, err := GetUserUploadsJSON(username)
if err != nil {
panic(err)
return nil, 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(`https://www.tiktok.com/@`+username),
// Execute url grabber script
chromedp.EvaluateAsDevTools(utils.ReadFileAsString("scraper.js"), &jsOutput),
chromedp.EvaluateAsDevTools("bootstrapIteratingVideos()", &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.ParseUploads(jsOutput)
return models.ParseUploads(actionOutput), nil
}
// GetUserUploadsJSON - Get user uploads scrape
func GetUserUploadsJSON(username string) (string, error) {
jsMethod := fmt.Sprintf("bootstrapIteratingVideos(%d)", config.Config.Limit)
actionOutput, err := executeClientAction(`https://www.tiktok.com/@`+username, jsMethod)
if err != nil {
return "", err
}
return actionOutput, nil
}

View File

@@ -1,58 +1,14 @@
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")
func GetVideoDetails(videoURL string) (models.Upload, error) {
actionOutput, err := executeClientAction(videoURL, "bootstrapGetCurrentVideo()")
if err != nil {
panic(err)
return models.Upload{}, 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)
return models.ParseUpload(actionOutput), nil
}

60
main.go
View File

@@ -1,62 +1,20 @@
package main
import (
client "./client"
models "./models"
utils "./utils"
"fmt"
"regexp"
"strings"
config "./models/config"
workflows "./workflows"
)
func main() {
models.GetConfig()
url := models.Config.URL
config.GetConfig()
url := config.Config.URL
batchFilePath := config.Config.BatchFilePath
// 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)
// Batch file
if workflows.CanUseDownloadBatchFile(batchFilePath) {
workflows.DownloadBatchFile(batchFilePath)
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)
if models.Config.MetaData {
metadataPath := fmt.Sprintf("%s/%s.json", downloadDir, uploadID)
upload.WriteToFile(metadataPath)
}
}
func downloadUser() {
username := models.Config.URL
downloadDir := fmt.Sprintf("%s/%s", models.Config.OutputPath, username)
uploads := client.GetUserUploads(username)
utils.InitOutputDirectory(downloadDir)
for _, upload := range uploads {
downloadVideo(upload, downloadDir)
}
workflows.StartWorkflowByParameter(url)
}

View File

@@ -1,34 +0,0 @@
package models
import (
"flag"
"fmt"
"os"
)
// Config - Runtime configuration
var Config struct {
URL string
OutputPath string
Debug bool
MetaData bool
}
// GetConfig - Returns Config object
func GetConfig() {
outputPath := flag.String("output", "./downloads", "Output path")
debug := flag.Bool("debug", false, "Enables debug mode")
metadata := flag.Bool("metadata", false, "Write video metadata to a .json file")
flag.Parse()
args := flag.Args()
if len(args) < 1 {
fmt.Println("Usage: tiktok-dl [OPTIONS] TIKTOK_USERNAME|TIKTOK_URL")
os.Exit(2)
}
Config.URL = flag.Args()[len(args)-1]
Config.OutputPath = *outputPath
Config.Debug = *debug
Config.MetaData = *metadata
}

60
models/config/config.go Normal file
View File

@@ -0,0 +1,60 @@
package config
import (
"flag"
"fmt"
"os"
)
// Config - Runtime configuration
var Config struct {
URL string
OutputPath string
BatchFilePath string
ArchiveFilePath string
Debug bool
MetaData bool
Quiet bool
JSONOnly bool
Deadline int
Limit int
}
// GetConfig - Returns Config object
func GetConfig() {
outputPath := flag.String("output", "./downloads", "Output path")
batchFilePath := flag.String("batch-file", "", "File containing URLs/Usernames to download, one value per line. Lines starting with '#', are considered as comments and ignored.")
archive := flag.String("archive", "", "Download only videos not listed in the archive file. Record the IDs of all downloaded videos in it.")
debug := flag.Bool("debug", false, "Enables debug mode")
metadata := flag.Bool("metadata", false, "Write video metadata to a .json file")
quiet := flag.Bool("quiet", false, "Supress output")
jsonOnly := flag.Bool("json", false, "Just get JSON data from scraper (without video downloading)")
deadline := flag.Int("deadline", 1500, "Sets the timout for scraper logic in seconds (used as a workaround for 'context deadline exceeded' error)")
limit := flag.Int("limit", 0, "Sets the videos count limit (useful when there too many videos from the user or by hashtag)")
flag.Parse()
args := flag.Args()
if len(args) < 1 && *batchFilePath == "" {
fmt.Println("Usage: tiktok-dl [OPTIONS] TIKTOK_USERNAME|TIKTOK_URL")
fmt.Println(" or: tiktok-dl [OPTIONS] -batch-file path/to/users.txt")
os.Exit(2)
}
if len(args) > 0 {
Config.URL = flag.Args()[len(args)-1]
} else {
Config.URL = ""
}
Config.OutputPath = *outputPath
Config.BatchFilePath = *batchFilePath
Config.ArchiveFilePath = *archive
Config.Debug = *debug
Config.MetaData = *metadata
Config.Quiet = *quiet
if *jsonOnly {
Config.Quiet = true
}
Config.JSONOnly = *jsonOnly
Config.Deadline = *deadline
Config.Limit = *limit
}

View File

@@ -2,9 +2,12 @@ package models
import (
"encoding/json"
"fmt"
"os"
"strings"
res "../resources"
checkErr "../utils/checkErr"
log "../utils/log"
)
// Upload - Upload object
@@ -12,6 +15,7 @@ type Upload struct {
URL string `json:"url"`
ShareLink string `json:"shareLink"`
Caption string `json:"caption"`
Uploader string `json:"uploader"`
Sound Sound `json:"sound"`
}
@@ -45,21 +49,16 @@ func (u Upload) GetUploadID() string {
func (u Upload) WriteToFile(outputPath string) {
bytes, err := json.Marshal(u)
if err != nil {
fmt.Printf("Could not serialize json for video: %s", u.GetUploadID())
fmt.Println()
log.Logf(res.ErrorCouldNotSerializeJSON, u.GetUploadID())
panic(err)
}
// Create the file
out, err := os.Create(outputPath)
if err != nil {
panic(err)
}
checkErr.CheckErr(err)
defer out.Close()
// Write to file
_, err = out.Write(bytes)
if err != nil {
panic(err)
}
checkErr.CheckErr(err)
}

View File

@@ -1,36 +1,70 @@
package models
import "testing"
import (
"os"
"testing"
testUtil "../unitTestUtil"
fileio "../utils/fileio"
)
// TestParseUploads - Test parsing
func TestParseUploads(t *testing.T) {
jsonStr := "[{\"shareLink\":\"some_share_link\", \"url\": \"some_url\"}]"
tu := testUtil.TestUtil{T: t}
jsonStr := "[{\"url\":\"some_url\",\"shareLink\":\"some_share_link\",\"caption\":\"some_caption\", \"uploader\": \"some.uploader\",\"sound\":{\"title\":\"some_title\",\"link\":\"some_link\"}}]"
actual := ParseUploads(jsonStr)
expectedLen := 1
if len(actual) != expectedLen {
t.Errorf("Array len incorrect: Expected %d, but got %d", expectedLen, len(actual))
}
tu.AssertInt(len(actual), 1, "Array len")
expectedShareLink := "some_share_link"
if actual[0].ShareLink != expectedShareLink {
t.Errorf("ShareLink is incorrect: Expected %s, but got %s", expectedShareLink, actual[0].ShareLink)
}
tu.AssertString(actual[0].URL, "some_url", "URL")
tu.AssertString(actual[0].Caption, "some_caption", "Caption")
tu.AssertString(actual[0].ShareLink, "some_share_link", "ShareLink")
tu.AssertString(actual[0].Uploader, "some.uploader", "Uploader")
expectedURL := "some_url"
if actual[0].URL != expectedURL {
t.Errorf("URL is incorrect: Expected %s, but got %s", expectedURL, actual[0].URL)
}
tu.AssertString(actual[0].Sound.Link, "some_link", "Sound.Link")
tu.AssertString(actual[0].Sound.Title, "some_title", "Sound.Title")
}
func TestParseUpload(t *testing.T) {
tu := testUtil.TestUtil{T: t}
jsonStr := "{\"url\":\"some_url\",\"shareLink\":\"some_share_link\",\"caption\":\"some_caption\",\"sound\":{\"title\":\"some_title\",\"link\":\"some_link\"}}"
actual := ParseUpload(jsonStr)
tu.AssertString(actual.URL, "some_url", "URL")
tu.AssertString(actual.Caption, "some_caption", "Caption")
tu.AssertString(actual.ShareLink, "some_share_link", "ShareLink")
tu.AssertString(actual.Sound.Link, "some_link", "Sound.Link")
tu.AssertString(actual.Sound.Title, "some_title", "Sound.Title")
}
func TestGetUploadID(t *testing.T) {
tu := testUtil.TestUtil{T: t}
var upload Upload
upload.ShareLink = "http://pikami.org/some_thing/some_upload_id"
expected := "some_upload_id"
actual := upload.GetUploadID()
if actual != expected {
t.Errorf("UploadId is incorrect: Expected %s, but got %s", expected, actual)
}
tu.AssertString(actual, "some_upload_id", "Upload ID")
}
func TestWriteToFile(t *testing.T) {
tu := testUtil.TestUtil{T: t}
expected := "{\"url\":\"some_url\",\"shareLink\":\"some_share_link\",\"caption\":\"some_caption\",\"uploader\":\"some.uploader\",\"sound\":{\"title\":\"some_title\",\"link\":\"some_link\"}}"
filePath := "test_file.txt"
upload := Upload{
URL: "some_url",
Caption: "some_caption",
ShareLink: "some_share_link",
Uploader: "some.uploader",
Sound: Sound{
Link: "some_link",
Title: "some_title",
},
}
upload.WriteToFile(filePath)
actual := fileio.ReadFileToString(filePath)
tu.AssertString(actual, expected, "File content")
os.Remove(filePath)
}

View File

@@ -3,12 +3,14 @@
"version": "0.0.1",
"scripts": {
"install-dependencies": "go get -v -t -d ./...",
"test": "go test -v ./models",
"test:coverage": "go test -short -coverprofile=cov.out ./models ./utils",
"test": "go test -v ./models && go test -v ./utils",
"clean": "rm -rf out",
"build:scraper": "node node_modules/terser/bin/terser -c -m -- scraper.js > out/scraper.js",
"build:app": "go build -o out/ -v .",
"build:dist": "mkdir out && npm run build:app && npm run build:scraper",
"build": "go build -v ."
"build": "go build -v .",
"sonar": "sonar-scanner -Dsonar.login=${SONAR_LOGIN} -Dproject.settings=.sonar/sonar-project.properties"
},
"dependencies": {
"terser": "^4.6.3"

13
resources/strings.go Normal file
View File

@@ -0,0 +1,13 @@
package resources
// ErrorCouldNotSerializeJSON -
var ErrorCouldNotSerializeJSON = "Could not serialize json for video: %s\n"
// ErrorCouldNotRecogniseURL -
var ErrorCouldNotRecogniseURL = "Could not recognise URL format of string %s"
// ErrorCouldNotGetUserUploads -
var ErrorCouldNotGetUserUploads = "Failed to get user uploads: %s\n"
// ErrorPathNotFound -
var ErrorPathNotFound = "File path %s not found."

View File

@@ -1,20 +1,23 @@
optStrings = {
selectors: {
feedLoading: 'div.tiktok-loading.feed-loading',
modalArrowLeft: 'div.video-card-modal > div > img.arrow-right',
modalArrowRight: 'div.video-card-modal > div > img.arrow-right',
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',
modalShareInput: '.copy-link-container > input',
modalCaption: 'div.video-card-big > div.content-container > div.video-meta-info > h1',
modalSoundLink: 'div.content-container > div.video-meta-info > h2.music-info > a',
modalUploader: '.user-username',
videoPlayer: 'div.video-card-container > div > div > video',
videoShareInput: 'div.content-container.border > div.copy-link-container > input',
videoCaption: 'div.content-container.border > div.video-meta-info > h1',
videoSoundLink: 'div.content-container.border > div.video-meta-info > h2.music-info > a',
videoUploader: '.user-username',
},
classes: {
feedVideoItem: 'video-feed-item-wrapper',
modalCloseDisabled: 'disabled',
titleMessage: 'title',
},
tags: {
resultTag: 'video_urls',
@@ -23,13 +26,38 @@ optStrings = {
attributes: {
src: "src",
},
tiktokMessages: [
"Couldn't find this account",
"No videos yet",
"Video currently unavailable",
],
};
currentState = {
preloadCount: 0,
finished: false,
limit: 0
};
checkForErrors = function() {
var titles = document.getElementsByClassName(optStrings.classes.titleMessage);
debugger;
if (titles && titles.length) {
var error = Array.from(titles).find(x => optStrings.tiktokMessages.includes(x.textContent)).textContent;
if (error) {
createVidUrlElement("ERR: " + error);
return true;
}
}
return false;
};
createVidUrlElement = function(outputObj) {
var urlSetElement = document.createElement(optStrings.tags.resultTag);
urlSetElement.innerText = JSON.stringify(outputObj);
document.getElementsByTagName(optStrings.tags.resultParentTag)[0].appendChild(urlSetElement);
}
currentState.finished = true;
};
buldVidUrlArray = function(finishCallback) {
var feedItem = document.getElementsByClassName(optStrings.classes.feedVideoItem)[0];
@@ -38,8 +66,14 @@ buldVidUrlArray = function(finishCallback) {
var videoArray = [];
var intervalID = window.setInterval(x => {
videoArray.push(getCurrentModalVideo());
var arrowRight = document.querySelectorAll(optStrings.selectors.modalArrowLeft)[0];
if(currentState.limit > 0) {
if (videoArray.length >= currentState.limit) {
window.clearInterval(intervalID);
document.querySelector(optStrings.selectors.modalClose).click();
finishCallback(videoArray);
}
}
var arrowRight = document.querySelectorAll(optStrings.selectors.modalArrowRight)[0];
if (arrowRight.classList.contains(optStrings.classes.modalCloseDisabled)) {
window.clearInterval(intervalID);
document.querySelector(optStrings.selectors.modalClose).click();
@@ -56,6 +90,7 @@ getCurrentModalVideo = function() {
var shareLink = document.querySelector(optStrings.selectors.modalShareInput).value;
var caption = document.querySelector(optStrings.selectors.modalCaption).textContent;
var soundLink = document.querySelector(optStrings.selectors.modalSoundLink);
var uploader = document.querySelector(optStrings.selectors.modalUploader).textContent;
var soundHref = soundLink.getAttribute("href");
var soundText = soundLink.text;
@@ -63,19 +98,22 @@ getCurrentModalVideo = function() {
url: vidUrl,
shareLink: shareLink,
caption: caption,
uploader: uploader,
sound: {
title: soundText,
link: soundHref,
},
};
}
};
getCurrentVideo = function() {
if(checkForErrors()) return;
var player = document.querySelector(optStrings.selectors.videoPlayer);
var vidUrl = player.getAttribute(optStrings.attributes.src);
var shareLink = document.querySelector(optStrings.selectors.videoShareInput).value;
var caption = document.querySelector(optStrings.selectors.videoCaption).textContent;
var soundLink = document.querySelector(optStrings.selectors.videoSoundLink);
var uploader = document.querySelector(optStrings.selectors.videoUploader).textContent;
var soundHref = soundLink.getAttribute("href");
var soundText = soundLink.text;
@@ -83,23 +121,38 @@ getCurrentVideo = function() {
url: vidUrl,
shareLink: shareLink,
caption: caption,
uploader: uploader,
sound: {
title: soundText,
link: soundHref,
},
};
}
};
scrollBottom = () => window.scrollTo(0, document.body.scrollHeight);
scrollWhileNew = function(finishCallback) {
var state = { count: 0 };
var intervalID = window.setInterval(x => {
scrollBottom();
var oldCount = state.count;
state.count = document.getElementsByClassName(optStrings.classes.feedVideoItem).length;
if(currentState.limit > 0) {
if (currentState.preloadCount >= currentState.limit || state.count >= currentState.limit) {
finishCallback(createVidUrlElement);
window.clearInterval(intervalID);
}
}
if(checkForErrors()) {
window.clearInterval(intervalID);
return;
} else if (state.count == 0) {
return;
}
if (oldCount !== state.count) {
window.scrollTo(0, document.body.scrollHeight);
currentState.preloadCount = state.count;
} else {
if (document.querySelector(optStrings.selectors.feedLoading)) {
window.scrollTo(0, document.body.scrollHeight);
return;
}
window.clearInterval(intervalID);
@@ -108,7 +161,8 @@ scrollWhileNew = function(finishCallback) {
}, 1000);
};
bootstrapIteratingVideos = function() {
bootstrapIteratingVideos = function(limit) {
currentState.limit = limit;
scrollWhileNew(buldVidUrlArray);
return 'bootstrapIteratingVideos';
};
@@ -117,7 +171,7 @@ bootstrapGetCurrentVideo = function() {
var video = getCurrentVideo();
createVidUrlElement(video);
return 'bootstrapGetCurrentVideo';
}
};
init = () => {
const newProto = navigator.__proto__;

15
unitTestUtil/assert.go Normal file
View File

@@ -0,0 +1,15 @@
package unittestutil
// AssertString - Check if two strings match
func (tu *TestUtil) AssertString(actual string, expected string, name string) {
if actual != expected {
tu.T.Errorf("%s is incorrect: Expected '%s', but got '%s'", name, expected, actual)
}
}
// AssertInt - Check if two intagers match
func (tu *TestUtil) AssertInt(actual int, expected int, name string) {
if actual != expected {
tu.T.Errorf("%s is incorrect: Expected '%d', but got '%d'", name, expected, actual)
}
}

View File

@@ -0,0 +1,10 @@
package unittestutil
import (
"testing"
)
// TestUtil - Utility for testing
type TestUtil struct {
T *testing.T
}

54
utils/archive.go Normal file
View File

@@ -0,0 +1,54 @@
package utils
import (
models "../models"
config "../models/config"
fileio "./fileio"
log "./log"
)
// IsItemInArchive - Checks if the item is already archived
func IsItemInArchive(upload models.Upload) bool {
if len(RemoveArchivedItems([]models.Upload{upload})) == 0 {
return true
}
return false
}
// RemoveArchivedItems - Returns items slice without archived items
func RemoveArchivedItems(uploads []models.Upload) []models.Upload {
archiveFilePath := config.Config.ArchiveFilePath
if archiveFilePath == "" || !fileio.CheckIfExists(archiveFilePath) {
return uploads
}
removeArchivedItemsDelegate := func(archivedItem string) {
for i, upload := range uploads {
if upload.GetUploadID() == archivedItem {
uploads = append(uploads[:i], uploads[i+1:]...)
}
}
}
lenBeforeRemoval := len(uploads)
fileio.ReadFileLineByLine(archiveFilePath, removeArchivedItemsDelegate)
removedCount := lenBeforeRemoval - len(uploads)
if removedCount > 0 {
log.Logf("%d items, found in archive. Skipping...\n", removedCount)
}
return uploads
}
// AddItemToArchive - Adds item to archived list
func AddItemToArchive(uploadID string) {
archiveFilePath := config.Config.ArchiveFilePath
if archiveFilePath == "" {
return
}
fileio.AppendToFile(uploadID, archiveFilePath)
}

View File

@@ -0,0 +1,12 @@
package utils
import (
"log"
)
// CheckErr - Checks if error and log
func CheckErr(err error) {
if err != nil {
log.Fatal(err)
}
}

View File

@@ -4,28 +4,23 @@ import (
"io"
"net/http"
"os"
checkErr "./checkErr"
)
// DownloadFile - Downloads content from `url` and stores it in `outputPath`
func DownloadFile(outputPath string, url string) {
// Get the data
resp, err := http.Get(url)
if err != nil {
panic(err)
}
checkErr.CheckErr(err)
defer resp.Body.Close()
// Create the file
out, err := os.Create(outputPath)
if err != nil {
panic(err)
}
checkErr.CheckErr(err)
defer out.Close()
// Write the body to file
_, err = io.Copy(out, resp.Body)
if err != nil {
panic(err)
}
checkErr.CheckErr(err)
}

View File

@@ -1,21 +0,0 @@
package utils
import (
"os"
)
// CheckIfExists - Checks if file or directory exists
func CheckIfExists(path string) bool {
if _, err := os.Stat(path); os.IsNotExist(err) {
return false
}
return true
}
// InitOutputDirectory - Creates output directory
func InitOutputDirectory(path string) {
if _, err := os.Stat(path); os.IsNotExist(err) {
os.MkdirAll(path, os.ModePerm)
}
}

64
utils/fileio/fileio.go Normal file
View File

@@ -0,0 +1,64 @@
package utils
import (
"bufio"
"io/ioutil"
"os"
checkErr "../checkErr"
)
type delegateString func(string)
// CheckIfExists - Checks if file or directory exists
func CheckIfExists(path string) bool {
if _, err := os.Stat(path); os.IsNotExist(err) {
return false
}
return true
}
// InitOutputDirectory - Creates output directory
func InitOutputDirectory(path string) {
if _, err := os.Stat(path); os.IsNotExist(err) {
os.MkdirAll(path, os.ModePerm)
}
}
// ReadFileToString - Reads file and returns content
func ReadFileToString(path string) string {
content, err := ioutil.ReadFile(path)
if err != nil {
panic(err)
}
return string(content)
}
// ReadFileLineByLine - Reads file line by line and calls delegate
func ReadFileLineByLine(path string, delegate delegateString) {
file, err := os.Open(path)
checkErr.CheckErr(err)
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
delegate(scanner.Text())
}
if err := scanner.Err(); err != nil {
panic(err)
}
}
// AppendToFile - Appends line to file
func AppendToFile(str string, filePath string) {
f, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
checkErr.CheckErr(err)
defer f.Close()
if _, err := f.WriteString(str + "\n"); err != nil {
checkErr.CheckErr(err)
}
}

16
utils/getHashtag.go Normal file
View File

@@ -0,0 +1,16 @@
package utils
import (
res "../resources"
"fmt"
"strings"
)
// GetHashtagFromURL - Get's tag name from passed url
func GetHashtagFromURL(str string) string {
if match := strings.Contains(str, "/tag/"); match {
return strings.Split(str, "/tag/")[1]
}
panic(fmt.Sprintf(res.ErrorCouldNotRecogniseURL, str))
}

28
utils/getUsername.go Normal file
View File

@@ -0,0 +1,28 @@
package utils
import (
config "../models/config"
res "../resources"
"fmt"
"regexp"
"strings"
)
// GetUsername - Get's username from passed URL param
func GetUsername() string {
return GetUsernameFromString(config.Config.URL)
}
// GetUsernameFromString - Get's username from passed param
func GetUsernameFromString(str string) string {
if match := strings.Contains(str, "/"); !match { // Not url
return strings.Replace(str, "@", "", -1)
}
if match, _ := regexp.MatchString(".+tiktok\\.com/@.+", str); match { // URL
stripedSuffix := strings.Split(str, "@")[1]
return strings.Split(stripedSuffix, "/")[0]
}
panic(fmt.Sprintf(res.ErrorCouldNotRecogniseURL, str))
}

37
utils/getUsername_test.go Normal file
View File

@@ -0,0 +1,37 @@
package utils
import (
config "../models/config"
testUtil "../unitTestUtil"
"testing"
)
func TestGetUsername(t *testing.T) {
testCaseDelegate := func(t *testing.T, url string, username string) {
tu := testUtil.TestUtil{T: t}
config.Config.URL = url
actual := GetUsername()
tu.AssertString(actual, username, "Username")
}
testVideoURL := func(t *testing.T) {
testCaseDelegate(t, "https://www.tiktok.com/@some_username/video/0000000000000000000", "some_username")
}
testProfileURL := func(t *testing.T) {
testCaseDelegate(t, "https://www.tiktok.com/@some_username", "some_username")
}
testPlainUsername := func(t *testing.T) {
testCaseDelegate(t, "some_username", "some_username")
}
testAtUsername := func(t *testing.T) {
testCaseDelegate(t, "@some_username", "some_username")
}
t.Run("Video URL", testVideoURL)
t.Run("Username URL", testProfileURL)
t.Run("Plain username", testPlainUsername)
t.Run("Username with @ suffix", testAtUsername)
}

32
utils/log/log.go Normal file
View File

@@ -0,0 +1,32 @@
package utils
import (
"fmt"
"os"
config "../../models/config"
)
// Log - Write to std out
func Log(a ...interface{}) {
if !config.Config.Quiet {
fmt.Println(a...)
}
}
// Logf - Write formated text
func Logf(format string, a ...interface{}) {
if !config.Config.Quiet {
fmt.Printf(format, a...)
}
}
// LogFatal - Write error and panic
func LogFatal(format string, a ...interface{}) {
panic(fmt.Sprintf(format, a...))
}
// LogErr - Write error
func LogErr(format string, a ...interface{}) {
fmt.Fprintf(os.Stderr, format, a...)
}

View File

@@ -2,14 +2,13 @@ package utils
import (
"io/ioutil"
"log"
checkErr "./checkErr"
)
// ReadFileAsString - Returns contents of given file
func ReadFileAsString(fileName string) string {
content, err := ioutil.ReadFile(fileName)
if err != nil {
log.Fatal(err)
}
checkErr.CheckErr(err)
return string(content)
}

View File

@@ -0,0 +1,29 @@
package workflows
import (
res "../resources"
fileio "../utils/fileio"
log "../utils/log"
)
// CanUseDownloadBatchFile - Check's if DownloadBatchFile can be used
func CanUseDownloadBatchFile(batchFilePath string) bool {
return batchFilePath != ""
}
// DownloadBatchFile - Download items from batch file
func DownloadBatchFile(batchFilePath string) {
if !fileio.CheckIfExists(batchFilePath) {
log.LogFatal(res.ErrorPathNotFound, batchFilePath)
}
fileio.ReadFileLineByLine(batchFilePath, downloadItem)
}
func downloadItem(batchItem string) {
if batchItem[0] == '#' {
return
}
StartWorkflowByParameter(batchItem)
}

View File

@@ -0,0 +1,52 @@
package workflows
import (
"fmt"
"strings"
client "../client"
config "../models/config"
res "../resources"
utils "../utils"
fileio "../utils/fileio"
log "../utils/log"
)
// CanUseDownloadHashtag - Test's if this workflow can be used for parameter
func CanUseDownloadHashtag(url string) bool {
match := strings.Contains(url, "/tag/")
return match
}
// DownloadHashtag - Download videos marked with given hashtag
func DownloadHashtag(url string) {
uploads, err := client.GetHashtagUploads(url)
if err != nil {
log.LogErr(res.ErrorCouldNotGetUserUploads, err.Error())
return
}
uploads = utils.RemoveArchivedItems(uploads)
uploadCount := len(uploads)
hashtag := utils.GetHashtagFromURL(url)
downloadDir := fmt.Sprintf("%s/%s", config.Config.OutputPath, hashtag)
fileio.InitOutputDirectory(downloadDir)
for index, upload := range uploads {
downloadVideo(upload, downloadDir)
log.Logf("\r[%d/%d] Downloaded", index+1, uploadCount)
}
log.Log()
}
// GetHashtagJSON - Prints scraped info from hashtag
func GetHashtagJSON(url string) {
uploads, err := client.GetHashtagUploadsJSON(url)
if err != nil {
log.LogErr(res.ErrorCouldNotGetUserUploads, err.Error())
return
}
fmt.Printf("%s", uploads)
}

View File

@@ -0,0 +1,51 @@
package workflows
import (
"fmt"
"regexp"
client "../client"
config "../models/config"
res "../resources"
utils "../utils"
fileio "../utils/fileio"
log "../utils/log"
)
// CanUseDownloadMusic - Check's if DownloadMusic can be used for parameter
func CanUseDownloadMusic(url string) bool {
match, _ := regexp.MatchString(".com\\/music\\/.+", url)
return match
}
// DownloadMusic - Download all videos by given music
func DownloadMusic(url string) {
uploads, err := client.GetMusicUploads(url)
if err != nil {
log.LogErr(res.ErrorCouldNotGetUserUploads, err.Error())
return
}
uploads = utils.RemoveArchivedItems(uploads)
uploadCount := len(uploads)
for index, upload := range uploads {
username := utils.GetUsernameFromString(upload.Uploader)
downloadDir := fmt.Sprintf("%s/%s", config.Config.OutputPath, username)
fileio.InitOutputDirectory(downloadDir)
downloadVideo(upload, downloadDir)
log.Logf("\r[%d/%d] Downloaded", index+1, uploadCount)
}
log.Log()
}
// GetMusicJSON - Prints scraped info from music
func GetMusicJSON(url string) {
uploads, err := client.GetMusicUploadsJSON(url)
if err != nil {
log.LogErr(res.ErrorCouldNotGetUserUploads, err.Error())
return
}
fmt.Printf("%s", uploads)
}

View File

@@ -0,0 +1,27 @@
package workflows
import (
client "../client"
res "../resources"
log "../utils/log"
"regexp"
)
// CanUseDownloadShareLink - Check's if DownloadShareLink can be used
func CanUseDownloadShareLink(url string) bool {
match, _ := regexp.MatchString("vm.tiktok.com\\/.+", url)
return match
}
// DownloadShareLink - Download item by share link
func DownloadShareLink(url string) {
log.Logf("Resolving share link: %s\n", url)
finalURL, err := client.GetRedirectUrl(url)
if err != nil {
log.LogErr(res.ErrorCouldNotGetUserUploads, err.Error())
return
}
StartWorkflowByParameter(finalURL)
}

53
workflows/downloadUser.go Normal file
View File

@@ -0,0 +1,53 @@
package workflows
import (
"fmt"
"regexp"
"strings"
client "../client"
config "../models/config"
res "../resources"
utils "../utils"
fileio "../utils/fileio"
log "../utils/log"
)
// CanUseDownloadUser - Test's if this workflow can be used for parameter
func CanUseDownloadUser(url string) bool {
isURL := strings.Contains(url, "/")
match, _ := regexp.MatchString(".+com\\/@[^\\/]+", url)
return !isURL || match
}
// DownloadUser - Download all user's videos
func DownloadUser(username string) {
uploads, err := client.GetUserUploads(username)
if err != nil {
log.LogErr(res.ErrorCouldNotGetUserUploads, err.Error())
return
}
uploads = utils.RemoveArchivedItems(uploads)
uploadCount := len(uploads)
downloadDir := fmt.Sprintf("%s/%s", config.Config.OutputPath, username)
fileio.InitOutputDirectory(downloadDir)
for index, upload := range uploads {
downloadVideo(upload, downloadDir)
log.Logf("\r[%d/%d] Downloaded", index+1, uploadCount)
}
log.Log()
}
// GetUserVideosJSON - Prints scraped info from user
func GetUserVideosJSON(username string) {
uploads, err := client.GetUserUploadsJSON(username)
if err != nil {
log.LogErr(res.ErrorCouldNotGetUserUploads, err.Error())
return
}
fmt.Printf("%s", uploads)
}

View File

@@ -0,0 +1,58 @@
package workflows
import (
"fmt"
"regexp"
client "../client"
models "../models"
config "../models/config"
res "../resources"
utils "../utils"
fileio "../utils/fileio"
log "../utils/log"
)
// CanUseDownloadSingleVideo - Check's if DownloadSingleVideo can be used for parameter
func CanUseDownloadSingleVideo(url string) bool {
match, _ := regexp.MatchString("\\/@.+\\/video\\/[0-9]+", url)
return match
}
// DownloadSingleVideo - Downloads single video
func DownloadSingleVideo(url string) {
username := utils.GetUsernameFromString(url)
upload, err := client.GetVideoDetails(url)
if err != nil {
log.LogErr(res.ErrorCouldNotGetUserUploads, err.Error())
return
}
if utils.IsItemInArchive(upload) {
return
}
downloadDir := fmt.Sprintf("%s/%s", config.Config.OutputPath, username)
fileio.InitOutputDirectory(downloadDir)
downloadVideo(upload, downloadDir)
log.Log("[1/1] Downloaded\n")
}
// DownloadVideo - Downloads one video
func downloadVideo(upload models.Upload, downloadDir string) {
uploadID := upload.GetUploadID()
downloadPath := fmt.Sprintf("%s/%s.mp4", downloadDir, uploadID)
if fileio.CheckIfExists(downloadPath) {
return
}
utils.DownloadFile(downloadPath, upload.URL)
if config.Config.MetaData {
metadataPath := fmt.Sprintf("%s/%s.json", downloadDir, uploadID)
upload.WriteToFile(metadataPath)
}
utils.AddItemToArchive(upload.GetUploadID())
}

View File

@@ -0,0 +1,57 @@
package workflows
import (
config "../models/config"
res "../resources"
utils "../utils"
log "../utils/log"
)
// StartWorkflowByParameter - Start needed workflow by given parameter
func StartWorkflowByParameter(url string) {
// Music
if CanUseDownloadMusic(url) {
if config.Config.JSONOnly {
GetMusicJSON(url)
} else {
DownloadMusic(url)
}
return
}
// Single video
if CanUseDownloadSingleVideo(url) {
DownloadSingleVideo(url)
return
}
// Tiktok user
if CanUseDownloadUser(url) {
if config.Config.JSONOnly {
GetUserVideosJSON(utils.GetUsernameFromString(url))
} else {
DownloadUser(utils.GetUsernameFromString(url))
}
return
}
// Tiktok hashtag
if CanUseDownloadHashtag(url) {
if config.Config.JSONOnly {
GetHashtagJSON(url)
} else {
DownloadHashtag(url)
}
return
}
// Share URL
if CanUseDownloadShareLink(url) {
DownloadShareLink(url)
return
}
log.LogFatal(res.ErrorCouldNotRecogniseURL, url)
}