Skip to content

Commit

Permalink
v1.1.2: initial playlist support
Browse files Browse the repository at this point in the history
  • Loading branch information
lostdusty committed Nov 25, 2024
1 parent f661c1b commit df15c41
Show file tree
Hide file tree
Showing 5 changed files with 137 additions and 70 deletions.
9 changes: 8 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,11 @@
go.work

# Win-get manifests
manifests/
manifests/

# media files
*.opus
*.mp3
*.mp4
*.webm
*.wav
21 changes: 14 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@ Planned features for cobalt-cli:


## Usage
cobalt-cli is similar to yt-dlp, just use `cobalt [url]`. If you use `cobalt help`, it will now show the help message.
cobalt-cli is similar to yt-dlp, just use `cobalt [url]`. If you use `cobalt help`, it will just show the help message.

To save a file to the current directory, use the `-s` flag, like: `cobalt https://www.youtube.com/watch?v=n1a7o44WxNo -s`
By default cobalt-cli saves the request link to the current directory, use the `-s` flag to change to another directory, like: `cobalt https://www.youtube.com/watch?v=n1a7o44WxNo -s ..\Videos`

### Help
```
Expand All @@ -59,9 +59,9 @@ usage: cobalt-cli [-h|--help] [url "<value>"] [-c|--video-codec (av1|vp9|h264)]
[-p|--filename-pattern (basic|pretty|nerdy|classic)]
[-m|--mode (auto|audio|mute)] [-x|--proxy]
[-d|--disable-metadata] [-t|--tiktok-h265]
[-T|--tiktok-full-audio] [-g|--gif] [-s|--save] [-a|--api
"<value>"] [-i|--instances] [-v|--verbose] [-k|--key
"<value>"] [-b|--benchmark]
[-T|--tiktok-full-audio] [-g|--gif] [-s|--save "<value>"]
[-a|--api "<value>"] [-i|--instances] [-v|--verbose]
[-k|--key "<value>"] [-b|--benchmark] [-P|--print]
save what you want, directly from the terminal, no unwanted
distractions involved. powered by cobalt's api
Expand Down Expand Up @@ -101,7 +101,9 @@ Arguments:
-T --tiktok-full-audio Download TikTok videos with the original sound used
in a TikTok video. Default: false
-g --gif Convert Twitter videos to GIFs. Default: false
-s --save Save the downloaded file to disk. Default: true
-s --save What folder to save the file to. If not provided,
will use the current directory. Default:
D:\Docs\GitHub\cobalt
-a --api Which API to use. Default is hyperdefined cobalt's
API. If you are hosting a custom API, or want to use
a different server, you can use it here. Default:
Expand All @@ -110,9 +112,12 @@ Arguments:
-v --verbose Enable verbose logging. Default: false
-k --key API key by the instance owner. You may need to
provide one to use download. Can be set with
COBALT_API_KEY environment variable. Default:
COBALT_API_KEY environment variable. If not
provided, will load from keychain. Default:
-b --benchmark Run a benchmark to test the download speed and
integrity. Default: false
-P --print Print the download link only, do not download the
file. Default: false
```

### Instances
Expand All @@ -138,6 +143,8 @@ After that, building with `go build` will automatically embed these files on the
Check out too:
- [tobalt, cobalt in typescript](https://github.com/tskau/tobalt)
- [tcobalt, cobalt cli in rust](https://github.com/khyerdev/tcobalt)
- [pybalt, cobalt cli & api in python](https://github.com/nichind/pybalt)
- [gobalt(2), another lib for cobalt in go](https://github.com/andresperezl/gobalt)


# About & Thanks
Expand Down
11 changes: 6 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ go 1.22
toolchain go1.23.1

require (
github.com/jedib0t/go-pretty/v6 v6.6.1
github.com/lostdusty/gobalt/v2 v2.0.4
github.com/schollz/progressbar/v3 v3.16.1
github.com/jedib0t/go-pretty/v6 v6.6.2
github.com/lostdusty/gobalt/v2 v2.0.8
github.com/schollz/progressbar/v3 v3.17.1
github.com/sirupsen/logrus v1.9.3
)

Expand All @@ -16,8 +16,9 @@ require (
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/stretchr/testify v1.9.0 // indirect
golang.org/x/sys v0.26.0 // indirect
golang.org/x/term v0.25.0 // indirect
golang.org/x/crypto v0.29.0 // indirect
golang.org/x/sys v0.27.0 // indirect
golang.org/x/term v0.26.0 // indirect
)

require (
Expand Down
12 changes: 12 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@ github.com/jedib0t/go-pretty/v6 v6.6.0 h1:wmZVuAcEkZRT+Aq1xXpE8IGat4vE5WXOMmBpbQ
github.com/jedib0t/go-pretty/v6 v6.6.0/go.mod h1:zbn98qrYlh95FIhwwsbIip0LYpwSG8SUOScs+v9/t0E=
github.com/jedib0t/go-pretty/v6 v6.6.1 h1:iJ65Xjb680rHcikRj6DSIbzCex2huitmc7bDtxYVWyc=
github.com/jedib0t/go-pretty/v6 v6.6.1/go.mod h1:zbn98qrYlh95FIhwwsbIip0LYpwSG8SUOScs+v9/t0E=
github.com/jedib0t/go-pretty/v6 v6.6.2 h1:27bLj3nRODzaiA7tPIxy9UVWHoPspFfME9XxgwiiNsM=
github.com/jedib0t/go-pretty/v6 v6.6.2/go.mod h1:zbn98qrYlh95FIhwwsbIip0LYpwSG8SUOScs+v9/t0E=
github.com/lostdusty/gobalt/v2 v2.0.3 h1:jCV/VXyz6wbK0lLQRosDsuSR7bBUPjdGhTu9OJeC3N4=
github.com/lostdusty/gobalt/v2 v2.0.3/go.mod h1:LiuOQrhZ81oUD8EtVIOXvdI0+iibamw0XYUeOhe8ibw=
github.com/lostdusty/gobalt/v2 v2.0.4 h1:MkIPh4zuHUDo0qlqFPpxDtyK3NUEI2cIGmjlLmLXar8=
github.com/lostdusty/gobalt/v2 v2.0.4/go.mod h1:LiuOQrhZ81oUD8EtVIOXvdI0+iibamw0XYUeOhe8ibw=
github.com/lostdusty/gobalt/v2 v2.0.8 h1:OYcjmKogUS+A4g8UqTFeCo8eRXalnPwrCRuoxm72Qgs=
github.com/lostdusty/gobalt/v2 v2.0.8/go.mod h1:LiuOQrhZ81oUD8EtVIOXvdI0+iibamw0XYUeOhe8ibw=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
Expand All @@ -25,6 +29,8 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/schollz/progressbar/v3 v3.16.1 h1:RnF1neWZFzLCoGx8yp1yF7SDl4AzNDI5y4I0aUJRrZQ=
github.com/schollz/progressbar/v3 v3.16.1/go.mod h1:I2ILR76gz5VXqYMIY/LdLecvMHDPVcQm3W/MSKi1TME=
github.com/schollz/progressbar/v3 v3.17.1 h1:bI1MTaoQO+v5kzklBjYNRQLoVpe0zbyRZNK6DFkVC5U=
github.com/schollz/progressbar/v3 v3.17.1/go.mod h1:RzqpnsPQNjUyIgdglUjRLgD7sVnxN1wpmBMV+UiEbL4=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
Expand All @@ -33,13 +39,19 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tgoncuoglu/argparse v0.0.0-20221031134704-ee5bd450c7a2 h1:rm0HxutQnzd1+0MeWtUzzWyty7GQ+bsj2uJbHpIJcrU=
github.com/tgoncuoglu/argparse v0.0.0-20221031134704-ee5bd450c7a2/go.mod h1:Y4qUI357fa9S6td9QzayjCtO8uN0Ft7A/S8s0s+DLGI=
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
Expand Down
154 changes: 97 additions & 57 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,16 @@ import (
"github.com/tgoncuoglu/argparse"
)

var version = "2.0.1"
var version = "1.1.2"
var useragent = fmt.Sprintf("cobalt-cli/%v (+https://github.com/lostdusty/cobalt; go/%v; %v/%v)", version, runtime.Version(), runtime.GOOS, runtime.GOARCH)

func init() {
log.SetFormatter(&log.TextFormatter{
ForceColors: true,
FullTimestamp: true,
})
}

func main() {
cobaltParser := argparse.NewParser("cobalt-cli", "save what you want, directly from the terminal, no unwanted distractions involved. powered by cobalt's api")
cobaltParser.ExitOnHelp(true)
Expand All @@ -43,7 +50,10 @@ func main() {
},
Help: "url to save",
})

pwd, err := os.Getwd()
if err != nil {
pwd = "."
}
youtubeVideoCodec := cobaltParser.Selector("c", "video-codec", []string{"av1", "vp9", "h264"}, &argparse.Options{
Required: false,
Help: "Video codec to be used. Applies only to youtube downloads. AV1: 8K/HDR, lower support | VP9: 4K/HDR, best quality | H264: 1080p, works everywhere",
Expand Down Expand Up @@ -99,10 +109,10 @@ func main() {
Help: "Convert Twitter videos to GIFs",
Default: false,
})
saveToDisk := cobaltParser.Flag("s", "save", &argparse.Options{
saveToDisk := cobaltParser.String("s", "save", &argparse.Options{
Required: false,
Help: "Save the downloaded file to disk",
Default: true,
Help: "What folder to save the file to. If not provided, will use the current directory",
Default: pwd,
})
apiUrl := cobaltParser.String("a", "api", &argparse.Options{
Required: false,
Expand All @@ -121,26 +131,26 @@ func main() {
})
apiKey := cobaltParser.String("k", "key", &argparse.Options{
Required: false,
Help: "API key by the instance owner. You may need to provide one to use download. Can be set with COBALT_API_KEY environment variable",
Help: "API key by the instance owner. You may need to provide one to use download. Can be set with COBALT_API_KEY environment variable. If not provided, will load from keychain",
Default: gobalt.ApiKey,
})
flagBenchmark := cobaltParser.Flag("b", "benchmark", &argparse.Options{
Required: false,
Help: "Run a benchmark to test the download speed and integrity",
Default: false,
})
printOnly := cobaltParser.Flag("P", "print", &argparse.Options{
Required: false,
Help: "Print the download link only, do not download the file",
Default: false,
})

err := cobaltParser.Parse(os.Args)
err = cobaltParser.Parse(os.Args)
if err != nil {
fmt.Println(err)
fmt.Println(cobaltParser.Usage(err))
return
}

log.SetFormatter(&log.TextFormatter{
ForceColors: true,
FullTimestamp: true,
})

if *debugVerbose {
log.SetLevel(log.DebugLevel)
}
Expand All @@ -161,6 +171,7 @@ func main() {
log.Debug("API key was provided via flag, setting it to gobalt")
gobalt.ApiKey = *apiKey
log.Debugf("Key from flag: %v | Key from Gobalt: %v | Key from COBALT_API_KEY: %v", *apiKey, gobalt.ApiKey, os.Getenv("COBALT_API_KEY"))

}

gobalt.CobaltApi = *apiUrl
Expand Down Expand Up @@ -239,66 +250,95 @@ func main() {
newDownload.TwitterConvertGif = *convertTwitterGif
log.Debugf("Options changed to: %v", newDownload)

err = fetchContent(newDownload, *saveToDisk)
//Check if the url is a playlist
if strings.Contains(*urlToDownload, "playlist") {
log.Debug("URL is a playlist, fetching playlist")
playlist, err := gobalt.GetYoutubePlaylist(*urlToDownload)
if err != nil {
log.Warnf("Error fetching playlist: %v, will try to fetch as a singular url...", err)
err = fetchContent(newDownload, *saveToDisk, *printOnly)
if err != nil {
log.Fatal(err)
return
}
}
log.Debugf("Playlist size: %v", len(playlist))

//Make an array of gobalt.Settings to download each video in the playlist
for n, video := range playlist {
newDownload.Url = video
err = fetchContent(newDownload, *saveToDisk, *printOnly)
if err != nil {
log.Errorln("\n", err)
}
log.Infof("\r\r%v of %v downloaded (%v%%)", n+1, len(playlist), (n+1)*100/len(playlist))
time.Sleep(400 * time.Millisecond)
}
fmt.Println()
log.Info("Playlist finished downloading!")
}

err = fetchContent(newDownload, *saveToDisk, *printOnly)
if err != nil {
log.Fatal(err)
return
}
log.Info("Download finished!")
}

func fetchContent(options gobalt.Settings, save bool) error {
log.Debug("Fetching content now, save to disk: ", save)
log.Info("Sending request to cobalt server...")
func fetchContent(options gobalt.Settings, save string, print bool) error {
log.Debug("Fetching content now, folder to save: ", save)
media, err := gobalt.Run(options)
if err != nil {
return err
}
log.Debug("Cobalt replied our request with the following url: ", media.URL)
fmt.Println(media.URL)
if save {
log.Info("Downloading the file to disk...")
if print {
fmt.Println(media.URL)
return nil
}

requestDownload, err := http.NewRequest("GET", media.URL, nil)
requestDownload.Header.Set("User-Agent", useragent)
log.Debug("Creating new request to download the file\nUser-Agent: ", useragent)
if err != nil {
return err
}
requestDownload, err := http.NewRequest("GET", media.URL, nil)
requestDownload.Header.Set("User-Agent", useragent)
log.Debug("Creating new request to download the file\nUser-Agent: ", useragent)
if err != nil {
return err
}

responseDownload, err := gobalt.Client.Do(requestDownload)
log.Debug("Sending request to download the file using gobalt client")
if err != nil {
return err
}
defer responseDownload.Body.Close()
responseDownload, err := gobalt.Client.Do(requestDownload)
log.Debug("Sending request to download the file using gobalt client")
if err != nil {
return err
}
defer responseDownload.Body.Close()

log.Debug("Request ok, status code: ", responseDownload.StatusCode)
log.Debug("Request ok, status code: ", responseDownload.StatusCode)

isResponseHTML := strings.Contains(responseDownload.Header.Get("Content-Type"), "text/html")
if responseDownload.StatusCode != http.StatusOK || isResponseHTML {
if isResponseHTML {
return fmt.Errorf("we got blocked trying to download the file, contact the instance owner if you think this is a mistake\nDetails: response is HTML (%s)", responseDownload.Header.Get("Content-Type"))
}
readBody, _ := io.ReadAll(responseDownload.Body)
log.Debugf("got status %v while download the file.\nBody:\n%v", responseDownload.Status, string(readBody))
return fmt.Errorf("error downloading the file: %s", responseDownload.Status)
isResponseHTML := strings.Contains(responseDownload.Header.Get("Content-Type"), "text/html")
if responseDownload.StatusCode != http.StatusOK || isResponseHTML {
if isResponseHTML {
return fmt.Errorf("we got blocked trying to download the file, contact the instance owner if you think this is a mistake\nDetails: response is HTML (%s)", responseDownload.Header.Get("Content-Type"))
}
readBody, _ := io.ReadAll(responseDownload.Body)
log.Debugf("got status %v while download the file.\nBody:\n%v", responseDownload.Status, string(readBody))
return fmt.Errorf("error downloading the file: %s", responseDownload.Status)
}

f, err := os.OpenFile(media.Filename, os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer f.Close()

bar := progressbar.DefaultBytes(
responseDownload.ContentLength,
"downloading "+media.Filename,
)
io.Copy(io.MultiWriter(f, bar), responseDownload.Body)
f.Sync()
fmt.Println()
log.Info("File downloaded successfully!")
f, err := os.OpenFile(save+"\\"+media.Filename, os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
log.Debug("Saving file to disk: ", f.Name())

defer f.Close()

bar := progressbar.DefaultBytes(
responseDownload.ContentLength,
"downloading "+media.Filename,
)
io.Copy(io.MultiWriter(f, bar), responseDownload.Body)
f.Sync()
fmt.Println()

return nil
}
Expand All @@ -311,9 +351,9 @@ func communityInstances() {
}
instancesTable := table.NewWriter()
instancesTable.SetOutputMirror(os.Stdout)
instancesTable.AppendHeader(table.Row{"API", "Score", "Trust", "Version (commit)", "Turnstile"})
instancesTable.AppendHeader(table.Row{"API", "Score", "Trust", "Version (commit)"})
for _, instance := range instances {
instancesTable.AppendRow(table.Row{instance.API, fmt.Sprintf("%.0f%%", instance.Score), instance.Trust, fmt.Sprintf("%v (%v)", instance.Version, instance.Commit), instance.Turnstile})
instancesTable.AppendRow(table.Row{instance.API, fmt.Sprintf("%v%%", instance.Score), instance.Trust, fmt.Sprintf("%v (%v)", instance.Version, instance.Commit)})
}
instancesTable.SetStyle(table.StyleRounded)
instancesTable.Render()
Expand Down

0 comments on commit df15c41

Please sign in to comment.