diff --git a/.gitignore b/.gitignore index 2c40c53..35fc792 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,11 @@ go.work # Win-get manifests -manifests/ \ No newline at end of file +manifests/ + +# media files +*.opus +*.mp3 +*.mp4 +*.webm +*.wav \ No newline at end of file diff --git a/README.md b/README.md index b7b26f8..5772bf7 100644 --- a/README.md +++ b/README.md @@ -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 ``` @@ -59,9 +59,9 @@ usage: cobalt-cli [-h|--help] [url ""] [-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 - ""] [-i|--instances] [-v|--verbose] [-k|--key - ""] [-b|--benchmark] + [-T|--tiktok-full-audio] [-g|--gif] [-s|--save ""] + [-a|--api ""] [-i|--instances] [-v|--verbose] + [-k|--key ""] [-b|--benchmark] [-P|--print] save what you want, directly from the terminal, no unwanted distractions involved. powered by cobalt's api @@ -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: @@ -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 @@ -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 diff --git a/go.mod b/go.mod index f9b72ab..130bcfa 100644 --- a/go.mod +++ b/go.mod @@ -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 ) @@ -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 ( diff --git a/go.sum b/go.sum index a3933d7..27c3ad5 100644 --- a/go.sum +++ b/go.sum @@ -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= @@ -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= @@ -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= diff --git a/main.go b/main.go index 0f3c6bb..9974b1f 100644 --- a/main.go +++ b/main.go @@ -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) @@ -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", @@ -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, @@ -121,7 +131,7 @@ 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{ @@ -129,18 +139,18 @@ func main() { 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) } @@ -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 @@ -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 } @@ -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()