diff --git a/.gitignore b/.gitignore index d44b260..2c40c53 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ *.syso *.linux *.mac +*.old # Test binary, built with `go test -c` *.test diff --git a/README.md b/README.md index 5e9fdf1..cf810f2 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ ![GitHub License](https://img.shields.io/github/license/princessmortix/cobalt?style=for-the-badge&logo=unlicense) -Unofficial [cobalt](https://cobalt.tools) command line client made in go. cobalt-cli uses [gobalt library](https://github.com/princessmortix/gobalt) for communication between your machine <-> cobalt servers. +Unofficial [cobalt](https://cobalt.tools) command line client made in go. cobalt-cli uses [gobalt library](https://github.com/lostdusty/gobalt) for communication between your machine <-> cobalt servers. - [Features](#features) - [Download](#downloading) @@ -20,10 +20,9 @@ Unofficial [cobalt](https://cobalt.tools) command line client made in go. cobalt ## Features - Get directly link from the service cdn (if possible); - More than 15 services supported; -- JSON output using the flag `-j` or `--json`; - Option to check status of cobalt servers; - Use custom cobalt instances (see https://instances.hyper.lol); -- Get dubbed youtube audio. +- Download the file directly to your computer. ## Download | **Platform/OS** | **Download link** | @@ -39,52 +38,45 @@ Alternatively, if you have Go installed, you can use `go install github.com/lost ## Roadmap Planned features for cobalt-cli: -- [x] Json output; - - [ ] Time expiration for the returned url. -- [ ] Option to save file to the current/custom folder, likely `-s` flag; - - [ ] Display progress bar to track download progress (when supported by cobalt). +- [x] Option to save file to the current/custom folder, likely `-s` flag; + - [x] Display progress bar to track download progress (when supported by cobalt). - [ ] Hability to use custom downloader program (wget, curl, got, etc); - [ ] Translations. ## Usage -cobalt-cli has two subcommands: -- download: downloads something using cobalt -- instances: lists all known cobalt instances +cobalt-cli is similar to yt-dlp, just use `cobalt [url]`. If you use `cobalt help`, it will now show the help message. -``` -Usage: cobalt -Commands: - download - Use this command to download something. - instances - Use this command to view stats about other cobalt instances. -Error: No command was provided. Please specify a command. -``` +To save a file to the current directory, use the `-s` flag, like: `cobalt https://www.youtube.com/watch?v=n1a7o44WxNo -s` -### Download +### Help ``` -usage: cobalt download [-h|--help] [-u|--url ""] [-c|--video-codec - (av1|vp9|h264)] [-q|--video-quality - (144|240|360|480|720|1080|1440|2160)] [-f|--audio-format - (opus|ogg|wav|mp3|best)] [-p|--filename-pattern - (basic|pretty|nerdy|classic)] [-a|--no-video] [-T|--tiktok-h265] - [-t|--full-tiktok-audio] [-v|--no-audio] [-d|--dubbed-audio] - [-m|--metadata] [-g|--gif] [-j|--json] [-s|--status] [-i|--api - ""] [-l|--language ""] [-b|--browser] - - download something using cobalt +usage: cobalt-cli [-h|--help] [url ""] [-c|--video-codec (av1|vp9|h264)] + [-q|--video-quality (144|240|360|480|720|1080|1440|2160)] + [-f|--audio-format (opus|ogg|wav|mp3|best)] + [-Q|--audio-quality (64|128|192|256|320)] + [-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] + + save what you want, directly from the terminal, no unwanted + distractions involved. powered by cobalt's api Arguments: -h --help Print help information - -u --url The url to download using cobalt + url to save -c --video-codec Video codec to be used. Applies only to youtube downloads. AV1: 8K/HDR, lower support | VP9: 4K/HDR, best quality | H264: 1080p, works everywhere. Default: h264 - -q --video-quality Quality of the video, also applies only to youtube + -q --video-quality Quality of the video, applies only to youtube downloads. Default: 1080 -f --audio-format Audio format/codec to be used. "best" doesn't - re-encodes the audio. Default: best + re-encodes audio. Default: best + -Q --audio-quality Audio quality in kbps. Default: 320 -p --filename-pattern File name pattern. Classic: youtube_yPYZpwSpKmA_1920x1080_h264.mp4 | audio: youtube_yPYZpwSpKmA_audio.mp3 // Basic: Video Title @@ -95,76 +87,30 @@ Arguments: youtube, yPYZpwSpKmA).mp4 | audio: Audio Title - Audio Author (soundcloud, 1242868615).mp3. Default: pretty - -a --no-video Downloads only the audio, and removes the video. - Default: false - -T --tiktok-h265 Downloads TikTok videos using h265 codec. Default: + -m --mode Mode to download the video. Auto: video with audio | + Audio: only audio | Mute: video without audio. + Default: auto + -x --proxy Tunnel the download through cobalt's servers, + bypassing potential restrictions and protecting your + identity and privacy. Default: false + -d --disable-metadata Disable metadata in the downloaded file. Default: false - -t --full-tiktok-audio Download the original sound used in a tiktok video. - Default: false - -v --no-audio Downloads only the video, without audio, when - possible. Default: false - -d --dubbed-audio Downloads youtube audio dubbed, if present. Change - the language using -l . Default: - false - -m --metadata Disables file metadata. Default: false - -g --gif Convert twitter gifs to .gif. Default: true - -j --json Output to stdout as json - -s --status Check status of the selected cobalt server, prints - and exits. All other options will be ignored, except - -j | --json. Default: false - -i --api Change the cobalt api url used. See others instances - in https://instances.hyper.lol. Default: - https://api.cobalt.tools - -l --language Downloads dubbed youtube audio according to the - language set following the ISO 639-1 format. Only - takes effect if -d was passed as an argument. - Default: en - -b --browser Downloads the requested media in your browser. - Default: false + -t --tiktok-h265 Use H265 codec for TikTok downloads. Default: false + -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 + -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: + https://cobalt-backend.canine.tools + -i --instances Show community instances and exit. Default: false + -v --verbose Enable verbose logging. Default: false ``` ### Instances -``` -usage: cobalt instances [-h|--help] [-j|--json] +The command changed, now to view other instances, use `cobalt -i` - get the list of cobalt instances - -Arguments: - - -h --help Print help information - -j --json Output to stdout as json -``` - -## JSON Output -Documentation for the json output of cobalt-cli. -### Download -All json output from the download subcommands follows this format: -```json -{ - "error": bool, - "message": "string", - "urls": ["string1", "string2", ...] -} -``` -Where: -| **name** | **type** | **info** | **example** | -|---|---|---|---| -| error | bool | true if something went wrong | "error":true | -| message | string | return error messages, otherwise "ok" | "message":"cobalt error: i couldn't connect to the service api. maybe it's down, or cobalt got blocked" | -| urls | []string | array of urls returned by the service, query scaped | "urls":["https%3A%2F%2Fus3-co.wuk.sh%2Fapi%2Fstream%3Ft%3D6kS3Xr97CAoqvPlBYX0r8%26e%3D1713849463113%26h%3DNHPfrLZ-BJejEnH2orowNy0zzTlVXSYw77RBhzIf0MU%26s%3DxvmTt9DTNl4wLslkfYfCUv6UDIPOTv9iZutl7ENM_dc%26i%3DuX7INLhsbzzofNxZaw6o7g"] | - -### Instances -Returns almost the original json from [https://instances.hyper.lol/](https://instances.hyper.lol/instances.json), except we add two extra keys: error and message, just like above. - -Example JSON: -```json -[{"error":false,"message":"success!"},[{"version":"7.12.6","commit":"50a98c8","branch":"current","name":"us3","url":"co.wuk.sh","cors":1,"startTime":"1713626380117","FrontendUrl":"cobalt.tools","ApiOnline":true,"FrontEndOnline":true},{"version":"7.12.6","commit":"50a98c8","branch":"current","name":"us-east","url":"cobalt.canine.tools","cors":1,"startTime":"1713837765475","FrontendUrl":"cobalt.canine.tools","ApiOnline":true,"FrontEndOnline":true},{"version":"7.12.6","commit":"50a98c8","branch":"current","name":"us-mw","url":"coapi.selfstacked.com","cors":1,"startTime":"1713626820678","FrontendUrl":"co.selfstacked.com","ApiOnline":true,"FrontEndOnline":true}]] -``` - -Error example: -```json -{"error":true,"message":"Get \"https://instances.hyper.lol/instances.json\": dial tcp: lookup instances.hyper.lol: no such host"} -``` ## Compiling Make sure you have the lastest go compiler. [Download it here](https://go.dev/dl). diff --git a/cobalt_test.go b/cobalt_test.go deleted file mode 100644 index 455d626..0000000 --- a/cobalt_test.go +++ /dev/null @@ -1,24 +0,0 @@ -package main - -import ( - "testing" - - "github.com/lostdusty/gobalt" -) - -//These tests allows me to test the application directly w/o the need to run `go run . [flags]`. - -func TestInstacesListing(t *testing.T) { - t.Log("testing instance listing") - err := getInstances(false) - if err != nil { - t.Fatalf("failed to fetch custom instaces: %v", err) - } -} - -func TestCobaltHealth(t *testing.T) { - err := checkStatus(gobalt.CobaltApi, true) - if err != nil { - t.Fatalf("failed to get cobalt instance health: %v", err) - } -} diff --git a/go.mod b/go.mod index 5c2faaf..d2683ab 100644 --- a/go.mod +++ b/go.mod @@ -1,17 +1,26 @@ module github.com/lostdusty/cobalt -go 1.21.6 +go 1.22 + +toolchain go1.23.1 require ( - github.com/akamensky/argparse v1.4.0 - github.com/emvi/iso-639-1 v1.1.0 - github.com/lostdusty/gobalt v1.0.7 - github.com/mergestat/timediff v0.0.3 + github.com/jedib0t/go-pretty/v6 v6.6.0 + github.com/lostdusty/gobalt/v2 v2.0.3 + github.com/sirupsen/logrus v1.9.3 ) require ( - github.com/jedib0t/go-pretty/v6 v6.5.9 - github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect github.com/rivo/uniseg v0.4.7 // indirect - golang.org/x/sys v0.21.0 // indirect + github.com/schollz/progressbar/v3 v3.16.1 // 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 +) + +require ( + github.com/mcuadros/go-version v0.0.0-20190830083331-035f6764e8d2 // indirect + github.com/tgoncuoglu/argparse v0.0.0-20221031134704-ee5bd450c7a2 ) diff --git a/go.sum b/go.sum index 4794ce4..34f821a 100644 --- a/go.sum +++ b/go.sum @@ -1,25 +1,42 @@ -github.com/akamensky/argparse v1.4.0 h1:YGzvsTqCvbEZhL8zZu2AiA5nq805NZh75JNj4ajn1xc= -github.com/akamensky/argparse v1.4.0/go.mod h1:S5kwC7IuDcEr5VeXtGPRVZ5o/FdhcMlQz4IZQuw64xA= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/emvi/iso-639-1 v1.1.0 h1:EhZiYVA+ysa/b7+0T2DD9hcX7E/5sh4o1KyDAIPu7VE= -github.com/emvi/iso-639-1 v1.1.0/go.mod h1:CSA53/Tx0xF9bk2DEA0Mr0wTdIxq7pqoVZgBOfoL5GI= -github.com/jedib0t/go-pretty/v6 v6.5.9 h1:ACteMBRrrmm1gMsXe9PSTOClQ63IXDUt03H5U+UV8OU= -github.com/jedib0t/go-pretty/v6 v6.5.9/go.mod h1:zbn98qrYlh95FIhwwsbIip0LYpwSG8SUOScs+v9/t0E= -github.com/lostdusty/gobalt v1.0.7 h1:8iL4eqp4NGn4lw9URYHD9DZYtIBMs2m81MWE+a9w/NM= -github.com/lostdusty/gobalt v1.0.7/go.mod h1:gv+Hmbv0SC3lWpJvUvxJYRmmcuBjlmhKCIRskk0lSjY= +github.com/jedib0t/go-pretty/v6 v6.6.0 h1:wmZVuAcEkZRT+Aq1xXpE8IGat4vE5WXOMmBpbQqERXw= +github.com/jedib0t/go-pretty/v6 v6.6.0/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/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mergestat/timediff v0.0.3 h1:ucCNh4/ZrTPjFZ081PccNbhx9spymCJkFxSzgVuPU+Y= -github.com/mergestat/timediff v0.0.3/go.mod h1:yvMUaRu2oetc+9IbPLYBJviz6sA7xz8OXMDfhBl7YSI= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mcuadros/go-version v0.0.0-20190830083331-035f6764e8d2 h1:YocNLcTBdEdvY3iDK6jfWXvEaM5OCKkjxPKoJRdB3Gg= +github.com/mcuadros/go-version v0.0.0-20190830083331-035f6764e8d2/go.mod h1:76rfSfYPWj01Z85hUf/ituArm797mNKcvINh1OlsZKo= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +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/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= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +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/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/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= +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= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index 18b4201..d4a4b15 100644 --- a/main.go +++ b/main.go @@ -1,392 +1,283 @@ package main import ( - "encoding/json" - "errors" "fmt" + "io" + "net/http" "net/url" "os" - "os/exec" "runtime" "strconv" "strings" - "time" - "github.com/akamensky/argparse" - iso6391 "github.com/emvi/iso-639-1" "github.com/jedib0t/go-pretty/v6/table" - "github.com/lostdusty/gobalt" - "github.com/mergestat/timediff" + "github.com/lostdusty/gobalt/v2" + "github.com/schollz/progressbar/v3" + log "github.com/sirupsen/logrus" + "github.com/tgoncuoglu/argparse" ) -func main() { - flagParser := argparse.NewParser("cobalt", "save what you love directly from command-line, no bullshit involved.") - doDownload := flagParser.NewCommand("download", "download something using cobalt") - getCobaltInstances := flagParser.NewCommand("instances", "get the list of cobalt instances") - - //jsonCobaltInstances := getCobaltInstances.Flag("J", "json-output", &argparse.Options{Required: false, Help: "return output in JSON format"}) - doDownload.ExitOnHelp(true) +var version = "2.0.0-Alpha" +var useragent = fmt.Sprintf("cobalt-cli/%v (+https://github.com/lostdusty/cobalt; go/%v; %v/%v)", version, runtime.Version(), runtime.GOOS, runtime.GOARCH) - URL := doDownload.String("u", "url", &argparse.Options{ +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) + urlToDownload := cobaltParser.StringPositional("url", &argparse.Options{ Required: false, - Help: "The url to download using cobalt", + Validate: func(args []string) error { + if args[0] == "help" { + return fmt.Errorf("\r%s", cobaltParser.Usage(nil)) + } + if args[0] == "version" { + return fmt.Errorf("\rcobalt-cli version %s\n%s", version, cobaltParser.Usage(nil)) + } + _, err := url.Parse(args[0]) + if err != nil { + return fmt.Errorf("invalid url, or parser failed to parse it: %s", err) + } + return nil + }, + Help: "url to save", }) - optionVideoCodec := doDownload.Selector("c", "video-codec", []string{"av1", "vp9", "h264"}, &argparse.Options{ + 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", Default: "h264", }) - - optionVideoQuality := doDownload.Selector("q", "video-quality", []string{"144", "240", "360", "480", "720", "1080", "1440", "2160"}, &argparse.Options{ + youtubeVideoQuality := cobaltParser.Selector("q", "video-quality", []string{"144", "240", "360", "480", "720", "1080", "1440", "2160"}, &argparse.Options{ Required: false, - Help: "Quality of the video, also applies only to youtube downloads", + Help: "Quality of the video, applies only to youtube downloads", Default: "1080", }) - - optionAudioFormat := doDownload.Selector("f", "audio-format", []string{"opus", "ogg", "wav", "mp3", "best"}, &argparse.Options{ + audioCodec := cobaltParser.Selector("f", "audio-format", []string{"opus", "ogg", "wav", "mp3", "best"}, &argparse.Options{ Required: false, - Help: "Audio format/codec to be used. \"best\" doesn't re-encodes the audio", + Help: "Audio format/codec to be used. \"best\" doesn't re-encodes audio", Default: "best", }) - - optionFilenamePattern := doDownload.Selector("p", "filename-pattern", []string{"basic", "pretty", "nerdy", "classic"}, &argparse.Options{ + audioQuality := cobaltParser.Selector("Q", "audio-quality", []string{"64", "128", "192", "256", "320"}, &argparse.Options{ + Required: false, + Help: "Audio quality in kbps", + Default: "320", + }) + fileNamePattern := cobaltParser.Selector("p", "filename-pattern", []string{"basic", "pretty", "nerdy", "classic"}, &argparse.Options{ Required: false, Help: "File name pattern. Classic: youtube_yPYZpwSpKmA_1920x1080_h264.mp4 | audio: youtube_yPYZpwSpKmA_audio.mp3 // Basic: Video Title (1080p, h264).mp4 | audio: Audio Title - Audio Author.mp3 // Pretty: Video Title (1080p, h264, youtube).mp4 | audio: Audio Title - Audio Author (soundcloud).mp3 // Nerdy: Video Title (1080p, h264, youtube, yPYZpwSpKmA).mp4 | audio: Audio Title - Audio Author (soundcloud, 1242868615).mp3", Default: "pretty", }) - - optionAudioOnly := doDownload.Flag("a", "no-video", &argparse.Options{ + typeDownload := cobaltParser.Selector("m", "mode", []string{"auto", "audio", "mute"}, &argparse.Options{ Required: false, - Help: "Downloads only the audio, and removes the video", - Default: false, + Help: "Mode to download the video. Auto: video with audio | Audio: only audio | Mute: video without audio", + Default: "auto", }) - - optionTikTokH265 := doDownload.Flag("T", "tiktok-h265", &argparse.Options{ + proxyDownload := cobaltParser.Flag("x", "proxy", &argparse.Options{ Required: false, - Help: "Downloads TikTok videos using h265 codec", + Help: "Tunnel the download through cobalt's servers, bypassing potential restrictions and protecting your identity and privacy", Default: false, }) - - optionFullTikTokAudio := doDownload.Flag("t", "full-tiktok-audio", &argparse.Options{ + disableMetadata := cobaltParser.Flag("d", "disable-metadata", &argparse.Options{ Required: false, - Help: "Download the original sound used in a tiktok video", + Help: "Disable metadata in the downloaded file", Default: false, }) - - optionVideoOnly := doDownload.Flag("v", "no-audio", &argparse.Options{ + tikTokH265 := cobaltParser.Flag("t", "tiktok-h265", &argparse.Options{ Required: false, - Help: "Downloads only the video, without audio, when possible", + Help: "Use H265 codec for TikTok downloads", Default: false, }) - - optionDubAudio := doDownload.Flag("d", "dubbed-audio", &argparse.Options{ + tikTokFullAudio := cobaltParser.Flag("T", "tiktok-full-audio", &argparse.Options{ Required: false, - Help: "Downloads youtube audio dubbed, if present. Change the language using -l ", + Help: "Download TikTok videos with the original sound used in a TikTok video", Default: false, }) - - optionDisableMetadata := doDownload.Flag("m", "metadata", &argparse.Options{ + convertTwitterGif := cobaltParser.Flag("g", "gif", &argparse.Options{ Required: false, - Help: "Disables file metadata", + Help: "Convert Twitter videos to GIFs", Default: false, }) - - optionConvertTwitterGif := doDownload.Flag("g", "gif", &argparse.Options{ + saveToDisk := cobaltParser.Flag("s", "save", &argparse.Options{ Required: false, - Help: "Convert twitter gifs to .gif", + Help: "Save the downloaded file to disk", Default: true, }) - - var outputJson, jsonOutput = doDownload.Flag("j", "json", &argparse.Options{ - Required: false, - Help: "Output to stdout as json", - }), - getCobaltInstances.Flag("j", "json", &argparse.Options{ - Required: false, - Help: "Output to stdout as json", - }) - - commandStatus := doDownload.Flag("s", "status", &argparse.Options{ - Required: false, - Help: "Check status of the selected cobalt server, prints and exits. All other options will be ignored, except -j | --json", - Default: false, - }) - - customCobaltApi := doDownload.String("i", "api", &argparse.Options{ + apiUrl := cobaltParser.String("a", "api", &argparse.Options{ Required: false, - Help: "Change the cobalt api url used. See others instances in https://instances.hyper.lol", + Help: "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: gobalt.CobaltApi, }) - - customLanguage := doDownload.String("l", "language", &argparse.Options{ + showCommunityInstances := cobaltParser.Flag("i", "instances", &argparse.Options{ Required: false, - Help: "Downloads dubbed youtube audio according to the language set following the ISO 639-1 format. Only takes effect if -d was passed as an argument", - Default: gobalt.UserLanguage, + Help: "Show community instances and exit", + Default: false, }) - - openInBrowser := doDownload.Flag("b", "browser", &argparse.Options{ + debugVerbose := cobaltParser.Flag("v", "verbose", &argparse.Options{ Required: false, - Help: "Downloads the requested media in your browser", + Help: "Enable verbose logging", Default: false, }) - err := flagParser.Parse(os.Args) - if err != nil && errors.Is(err, flagParser.Parse(os.Args)) { - fmt.Print(`Usage: cobalt -Commands: -  download  - Use this command to download something. -  instances - Use this command to view stats about other cobalt instances. -Error: No command was provided. Please specify a command. -`) + err := cobaltParser.Parse(os.Args) + if err != nil { + fmt.Println(err) return } - if getCobaltInstances.Happened() { - err := getInstances(*jsonOutput) - if err != nil { - if *jsonOutput { - fmt.Println(errorJson(err)) - return - } - fmt.Println(err) - return - } - return - } + log.SetFormatter(&log.TextFormatter{ + ForceColors: true, + FullTimestamp: true, + }) - if *commandStatus { - err := checkStatus(*customCobaltApi, *outputJson) - if err != nil { - if *jsonOutput { - fmt.Println(errorJson(err)) - return - } - fmt.Println(err) - return - } - return + if *debugVerbose { + log.SetLevel(log.DebugLevel) } - if *URL == "" { - fmt.Println(flagParser.Help(doDownload)) + if len(os.Args) < 2 { + log.Debug("No arguments provided, showing help") + fmt.Println(cobaltParser.Usage(nil)) return } - validateLanguage := iso6391.ValidCode(strings.ToLower(*customLanguage)) - if !validateLanguage { - if *outputJson { - fmt.Println(fmt.Errorf("invalid language code, check if the language code is following ISO 639-1 format")) - return - } - fmt.Println("Invalid language code: " + *customLanguage) + if *showCommunityInstances { + log.Debug("Flag to show community instances is set, showing instances") + communityInstances() return } - newSettings := gobalt.CreateDefaultSettings() - if *customCobaltApi != gobalt.CobaltApi { - gobalt.CobaltApi = *customCobaltApi + newDownload := gobalt.CreateDefaultSettings() + log.Debugf("Creating new cobalt download with default options: %v", newDownload) + gobalt.CobaltApi = *apiUrl + newDownload.Url = *urlToDownload + switch *youtubeVideoCodec { + case "av1": + newDownload.YoutubeVideoFormat = gobalt.AV1 + case "vp9": + newDownload.YoutubeVideoFormat = gobalt.VP9 + case "h264": + newDownload.YoutubeVideoFormat = gobalt.H264 + default: + newDownload.YoutubeVideoFormat = gobalt.H264 } - - switch *optionAudioFormat { + newDownload.VideoQuality, _ = strconv.Atoi(*youtubeVideoQuality) + switch *audioCodec { + case "opus": + newDownload.AudioFormat = gobalt.Opus case "ogg": - newSettings.AudioCodec = gobalt.Ogg + newDownload.AudioFormat = gobalt.Ogg case "wav": - newSettings.AudioCodec = gobalt.Wav + newDownload.AudioFormat = gobalt.Wav case "mp3": - newSettings.AudioCodec = gobalt.MP3 + newDownload.AudioFormat = gobalt.MP3 case "best": - newSettings.AudioCodec = gobalt.Best - case "opus": - newSettings.AudioCodec = gobalt.Opus - default: - newSettings.AudioCodec = gobalt.Best - } - switch *optionVideoCodec { - case "av1": - newSettings.VideoCodec = gobalt.AV1 - case "h264": - newSettings.VideoCodec = gobalt.H264 - case "vp9": - newSettings.VideoCodec = gobalt.VP9 + newDownload.AudioFormat = gobalt.Best default: - newSettings.VideoCodec = gobalt.H264 + newDownload.AudioFormat = gobalt.Best } - switch *optionFilenamePattern { - case "classic": - newSettings.FilenamePattern = gobalt.Classic + newDownload.AudioBitrate, _ = strconv.Atoi(*audioQuality) + switch *fileNamePattern { case "basic": - newSettings.FilenamePattern = gobalt.Basic + newDownload.FilenameStyle = gobalt.Basic case "pretty": - newSettings.FilenamePattern = gobalt.Pretty + newDownload.FilenameStyle = gobalt.Pretty case "nerdy": - newSettings.FilenamePattern = gobalt.Nerdy + newDownload.FilenameStyle = gobalt.Nerdy + case "classic": + newDownload.FilenameStyle = gobalt.Classic + default: + newDownload.FilenameStyle = gobalt.Pretty } - newSettings.AudioOnly = *optionAudioOnly - newSettings.ConvertTwitterGifs = *optionConvertTwitterGif - newSettings.DisableVideoMetadata = *optionDisableMetadata - newSettings.DubbedYoutubeAudio = *optionDubAudio - newSettings.FullTikTokAudio = *optionFullTikTokAudio - newSettings.TikTokH265 = *optionTikTokH265 - newSettings.Url = *URL - newSettings.VideoOnly = *optionVideoOnly - quality, err := strconv.Atoi(*optionVideoQuality) + switch *typeDownload { + case "auto": + newDownload.Mode = gobalt.Auto + case "audio": + newDownload.Mode = gobalt.Audio + case "mute": + newDownload.Mode = gobalt.Mute + default: + newDownload.Mode = gobalt.Auto + } + newDownload.Proxy = *proxyDownload + newDownload.DisableMetadata = *disableMetadata + newDownload.TikTokH265 = *tikTokH265 + newDownload.TikTokFullAudio = *tikTokFullAudio + newDownload.TwitterConvertGif = *convertTwitterGif + log.Debugf("Options changed to: %v", newDownload) + + err = fetchContent(newDownload, *saveToDisk) if err != nil { - if *outputJson { - fmt.Println(fmt.Errorf("expected int on flag -q, got something else: %s", *optionVideoQuality)) - return - } - fmt.Println(fmt.Errorf("expected int on flag -q, got something else: %s\nError details: %e", *optionVideoQuality, err)) + log.Fatal(err) return } - newSettings.VideoQuality = quality +} - cobaltRequest, err := gobalt.Run(newSettings) +func fetchContent(options gobalt.Settings, save bool) error { + log.Debug("Fetching content now, save to disk: ", save) + log.Info("Sending request to cobalt server...") + media, err := gobalt.Run(options) if err != nil { - if *outputJson { - fmt.Println(errorJson(err)) - return - } - fmt.Println(err) - return + return err } - - if *outputJson { - safeUrlOutput := make([]string, 0) - for _, u := range cobaltRequest.URLs { - safe := url.QueryEscape(u) - safeUrlOutput = append(safeUrlOutput, safe) + 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...") + + 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 } - if cobaltRequest.Status == "picker" { - unmarshalOutput := map[string]interface{}{"error": false, "message": cobaltRequest.Text, "urls": safeUrlOutput} - output, _ := json.Marshal(unmarshalOutput) - fmt.Println(string(output)) - return + 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() - unmarshalOutput := map[string]interface{}{"error": false, "message": cobaltRequest.Text, "urls": safeUrlOutput} - output, _ := json.Marshal(unmarshalOutput) - fmt.Println(string(output)) - return - } - - if cobaltRequest.Status == "picker" { - fmt.Println(cobaltRequest.URLs) - return - } + log.Debug("Request ok, status code: ", responseDownload.StatusCode) - if *openInBrowser { - for _, urls := range cobaltRequest.URLs { - err := openInDefaultBrowser(urls) - if err != nil { - fmt.Printf("Failed to open URL on default browser: %v\nWill print instead.", err) + 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) } - } - - fmt.Println(cobaltRequest.URL) -} -func checkStatus(api string, returnJson bool) error { - check, err := gobalt.CobaltServerInfo(api) - if err != nil { - if returnJson { + f, err := os.OpenFile(media.Filename, os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { return err } - return fmt.Errorf("failed to contact cobalt server at %s due of the following error %e", api, err) + defer f.Close() + + bar := progressbar.DefaultBytes( + responseDownload.ContentLength, + "downloading "+media.Filename, + ) + io.Copy(io.MultiWriter(f, bar), responseDownload.Body) + f.Sync() + log.Info("File downloaded successfully!") } - if returnJson { - respJson := map[string]interface{}{"error": false, - "message": "contact was successful", - "branch": check.Branch, - "commit": check.Commit, - "name": check.Name, - "startTime": check.StartTime, - "url": check.URL, - "Version": check.Version, - "Cors": fmt.Sprint(check.Cors), - } - outputJson, _ := json.Marshal(respJson) - fmt.Println(string(outputJson)) - return nil - } - - fmt.Printf("%s Status:\nBranch: %v\nCommit: %v\nName: %v\nStart time: %v (%v)\nURL: %v\nVersion: %v\nCors: %v", api, check.Branch, check.Commit, check.Name, time.UnixMilli(check.StartTime).Format(time.RFC1123), utilHumanTime(check.StartTime), check.URL, check.Version, check.Cors) return nil } -func errorJson(err error) string { - marshalThis := map[string]interface{}{"error": true, - "message": fmt.Sprintf("%s", err), - //"urls": make([]string, 0), - } - errorInJson, _ := json.Marshal(marshalThis) - return string(errorInJson) -} - -func openInDefaultBrowser(url string) error { - switch runtime.GOOS { - case "windows": - return exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() - case "darwin": - return exec.Command("start", url).Start() - default: - return exec.Command("xdg-open", url).Start() - } -} - -func getInstances(args bool) error { +func communityInstances() { instances, err := gobalt.GetCobaltInstances() if err != nil { - return err - } - - if args { - appendHeaders := map[string]interface{}{ - "error": false, - "message": "success!", - } - merged := make([]any, 0) - merged = append(merged, appendHeaders) - merged = append(merged, instances) - outJson, _ := json.Marshal(merged) - fmt.Println(string(outJson)) - return nil + log.Fatal("Error fetching community instances:", err) + return } - instancesTable := table.NewWriter() instancesTable.SetOutputMirror(os.Stdout) - instancesTable.AppendHeader(table.Row{"name", "api url (frontend)", "API/Front online", "cors", "since", "version (branch, commit)"}) - for _, v := range instances { - instancesTable.AppendRow(table.Row{v.Name, fmt.Sprintf("%v (front: %v)", v.URL, v.FrontendUrl), fmt.Sprintf("%v", resolveApiFrontOnline(v.ApiOnline, v.FrontEndOnline)), resolveCors(v.Cors), utilHumanTime(v.StartTime), fmt.Sprintf("%v (%v, %v)", v.Version, v.Branch, v.Commit)}) + instancesTable.AppendHeader(table.Row{"API", "Score", "Trust", "Version (commit)", "Turnstile"}) + 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.SetStyle(table.StyleRounded) instancesTable.Render() - - return nil -} - -func utilHumanTime(unixTime int64) string { - since := time.UnixMilli(unixTime) - return timediff.TimeDiff(since) -} - -func resolveApiFrontOnline(api, front bool) string { - a, f := "no", "no" - if api { - a = "yes" - } - if front { - f = "yes" - } - return fmt.Sprintf("%s/%s", a, f) -} - -func resolveCors(cors int) string { - if cors == 1 { - return "on" - } - return "off" } diff --git a/winres/winres.json b/winres/winres.json index 52120c4..3ef83cc 100644 --- a/winres/winres.json +++ b/winres/winres.json @@ -11,7 +11,7 @@ "0409": { "identity": { "name": "cobalt", - "version": "1.0.7" + "version": "2.0.0" }, "description": "cobalt cli version for https://cobalt.tools", "minimum-os": "vista", @@ -35,8 +35,8 @@ "#1": { "0000": { "fixed": { - "file_version": "1.0.0.7", - "product_version": "1.0.0.7", + "file_version": "2.0.0.0", + "product_version": "2.0.0.0", "flags": "Release" }, "info": { @@ -44,14 +44,14 @@ "Comments": "cobalt cli downloader", "CompanyName": "Lost Dusty", "FileDescription": "", - "FileVersion": "1.0.7", + "FileVersion": "2.0.0", "InternalName": "", "LegalCopyright": "", "LegalTrademarks": "", "OriginalFilename": "", "PrivateBuild": "", "ProductName": "cobalt cli downloader", - "ProductVersion": "1.0", + "ProductVersion": "2.0", "SpecialBuild": "" } }