├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── feature_request.yml └── workflows │ ├── go.yml │ └── todo.yml ├── .gitignore ├── README.md ├── constants.go ├── ffmpeg.go ├── github.go ├── go.mod ├── go.sum ├── logger.go ├── main.go ├── scripts ├── build-linux.bat ├── build.bat └── run.bat ├── updater.go └── util.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: Puyodead1 2 | ko_fi: puyodead1 3 | patreon: Puyodead1 4 | custom: https://www.buymeacoffee.com/puyodead1 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | title: "[Bug]: " 4 | labels: ["bug"] 5 | assignees: 6 | - Puyodead1 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | Thanks for taking the time to fill out this bug report! 12 | - type: dropdown 13 | id: branch 14 | attributes: 15 | label: Branch 16 | description: What branch are you using? 17 | options: 18 | - "main" 19 | - Other (Enter below) 20 | validations: 21 | required: true 22 | - type: dropdown 23 | id: os 24 | attributes: 25 | label: What operating systems are you seeing the problem on? 26 | multiple: true 27 | options: 28 | - Windows 29 | - Linux/Unix 30 | - MacOS 31 | - Other (Enter Below) 32 | - type: textarea 33 | id: version 34 | attributes: 35 | label: Program Version 36 | placeholder: "You can find this by running the program with just the -version argument: `udemy-dl-go -version`" 37 | description: "Ex: 2023-02-22.1677092994-git-d6cd1e2" 38 | validations: 39 | required: true 40 | - type: textarea 41 | id: what-happened 42 | attributes: 43 | label: What happened? 44 | description: Describe with as much detail as you can exactly what happened 45 | validations: 46 | required: true 47 | - type: textarea 48 | id: expected-result 49 | attributes: 50 | label: Expected Result 51 | description: What do you expect to happen if the bug didn't occur? 52 | validations: 53 | required: true 54 | - type: textarea 55 | id: logs 56 | attributes: 57 | label: Relevant log output 58 | description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. Remember to censor sensitive information before submitting! 59 | render: Shell 60 | - type: textarea 61 | id: other-information 62 | attributes: 63 | label: Other information 64 | description: Enter other information here such as an unlisted OS or branch 65 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest an idea for this project 3 | title: "[Feature Request]: " 4 | labels: ["enhancement"] 5 | assignees: 6 | - Puyodead1 7 | body: 8 | - type: checkboxes 9 | id: is-related-to-problem 10 | attributes: 11 | label: Is your feature request related to a problem? 12 | options: 13 | - label: "Yes" 14 | - label: "No" 15 | - type: textarea 16 | id: problem 17 | attributes: 18 | label: If yes, please describe 19 | placeholder: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 20 | validations: 21 | required: false 22 | - type: textarea 23 | id: solution 24 | attributes: 25 | label: Describe the solution you'd like 26 | placeholder: A clear and concise description of what you want to happen. 27 | validations: 28 | required: true 29 | - type: textarea 30 | id: alternatives 31 | attributes: 32 | label: Describe alternatives you've considered 33 | placeholder: A clear and concise description of any alternative solutions or features you've considered. 34 | validations: 35 | required: true 36 | - type: textarea 37 | id: additional-context 38 | attributes: 39 | label: Additional context 40 | placeholder: Add any other context or screenshots about the feature request here. 41 | validations: 42 | required: false 43 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | on: 3 | - push 4 | - pull_request 5 | 6 | jobs: 7 | build: 8 | if: "! contains(toJSON(github.event.commits.*.message), '[ci skip]')" 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | include: 13 | - os: windows 14 | arch: amd64 15 | uploadname: win-amd64 16 | - os: linux 17 | arch: amd64 18 | uploadname: linux-amd64 19 | - os: linux 20 | arch: arm64 21 | uploadname: linux-arm64 22 | - os: linux 23 | arch: arm 24 | uploadname: linux-arm 25 | - os: darwin 26 | arch: amd64 27 | uploadname: darwin-amd64 28 | 29 | steps: 30 | - uses: actions/checkout@v2 31 | - name: Set up Go 32 | uses: actions/setup-go@v2 33 | with: 34 | go-version: 1.18 35 | - name: Build ${{ matrix.os }} ${{ matrix.arch }} 36 | run: env GOOS=${{ matrix.os }} GOARCH=${{ matrix.arch }} go build -o udemy-dl-go -v -ldflags "-X main.version=`date -u +%Y-%m-%d-git-$(git rev-parse --short HEAD)`" ./... 37 | - uses: actions/upload-artifact@v2 38 | with: 39 | name: ${{ matrix.uploadname }} 40 | path: udemy-dl-go 41 | -------------------------------------------------------------------------------- /.github/workflows/todo.yml: -------------------------------------------------------------------------------- 1 | name: "TODO Issue Creator" 2 | on: 3 | push: 4 | pull_request: 5 | workflow_call: 6 | jobs: 7 | run: 8 | runs-on: "ubuntu-latest" 9 | steps: 10 | - uses: "actions/checkout@master" 11 | - name: "TODO to Issue" 12 | uses: "alstr/todo-to-issue-action@v4.6.3" 13 | id: "todo" 14 | with: 15 | AUTO_ASSIGN: true 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | dist/ 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # udemy-dl-go 2 | 3 | A WIP Udemy downloader written in Go 4 | 5 | I'm very new to Go, and this is my learning project. 6 | -------------------------------------------------------------------------------- /constants.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "path/filepath" 4 | 5 | // Udemy 6 | const COURSE_URL = "https://{portal_name}.udemy.com/api-2.0/courses/{course_id}/cached-subscriber-curriculum-items?fields[asset]=results,title,external_url,time_estimation,download_urls,slide_urls,filename,asset_type,captions,media_license_token,course_is_drmed,media_sources,stream_urls,body&fields[chapter]=object_index,title,sort_order&fields[lecture]=id,title,object_index,asset,supplementary_assets,view_html&page_size=10000" 7 | const COURSE_INFO_URL = "https://{portal_name}.udemy.com/api-2.0/courses/{course_id}/" 8 | const COURSE_SEARCH_URL = "https://{portal_name}.udemy.com/api-2.0/users/me/subscribed-courses?fields[course]=id,url,title,published_title&page=1&page_size=500&search={course_name}" 9 | const SUBSCRIBED_COURSES_URL = "https://{portal_name}.udemy.com/api-2.0/users/me/subscribed-courses/?ordering=-last_accessed&fields[course]=id,title,url&page=1&page_size=12" 10 | const MY_COURSES_URL = "https://{portal_name}.udemy.com/api-2.0/users/me/subscribed-courses?fields[course]=id,url,title,published_title&ordering=-last_accessed,-access_time&page=1&page_size=10000" 11 | const COLLECTION_URL = "https://{portal_name}.udemy.com/api-2.0/users/me/subscribed-courses-collections/?collection_has_courses=True&course_limit=20&fields[course]=last_accessed_time,title,published_title&fields[user_has_subscribed_courses_collection]=@all&page=1&page_size=1000" 12 | const LOGIN_URL = "https://www.udemy.com/join/login-popup/?ref=&display_type=popup&loc" 13 | 14 | // FFMPEG Windows 15 | const FFMPEG_WIN_LATEST_VERSION_URL = "https://www.gyan.dev/ffmpeg/builds/git-version" 16 | const FFMPEG_WIN_SHA_URL = "https://www.gyan.dev/ffmpeg/builds/packages/ffmpeg-%s-essentials_build.7z.sha256" 17 | const FFMPEG_WIN_URL = "https://www.gyan.dev/ffmpeg/builds/packages/ffmpeg-%s-essentials_build.7z" 18 | 19 | // FFMPEG Mac 20 | const FFMPEG_MAC_INFO_URL = "https://evermeet.cx/ffmpeg/info/ffmpeg/snapshot" // gets information about the latest snapshot 21 | const FFMPEG_MAC_VERSION_INFO_URL = "https://evermeet.cx/ffmpeg/info/ffmpeg/%s" // gets information about a specific version 22 | 23 | // Paths 24 | var FFMPEG_BIN_DIRECTORY = filepath.Join("bin", "ffmpeg") 25 | -------------------------------------------------------------------------------- /ffmpeg.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "path/filepath" 7 | "runtime" 8 | ) 9 | 10 | type FFMPEGMacVersionDownloadInfo struct { 11 | Url string `json:"url"` 12 | Size int `json:"size"` 13 | Sig string `json:"sig"` 14 | } 15 | 16 | type FFMPEGMacVersionDownload struct { 17 | SZ FFMPEGMacVersionDownloadInfo `json:"7z"` 18 | Zip FFMPEGMacVersionDownloadInfo `json:"zip"` 19 | } 20 | 21 | type FFMPEGMacVersion struct { 22 | Name string `json:"name"` 23 | Type string `json:"type"` 24 | Version string `json:"version"` 25 | Size int `json:"size"` 26 | Download FFMPEGMacVersionDownload `json:"download"` 27 | } 28 | 29 | // Gets the latest version number of FFMPEG for Windows 30 | func GetLatestWinFFMPEGVersion() (string, error) { 31 | // 32 | return GetText(FFMPEG_WIN_LATEST_VERSION_URL) 33 | } 34 | 35 | // Gets the latest version information of FFMPEG for Mac 36 | func GetLatestMacFFMPEGVersion() (string, error) { 37 | var err error 38 | release := FFMPEGMacVersion{} 39 | data, err := GetBytes(FFMPEG_MAC_INFO_URL) 40 | if err != nil { 41 | return "", err 42 | } 43 | 44 | err = json.Unmarshal(data, &version) 45 | if err != nil { 46 | return "", err 47 | } 48 | 49 | return release.Version, nil 50 | } 51 | 52 | // Gets the latest version of FFMPEG for Linux 53 | func GetLatestLinuxFFMPEGVersion() (string, error) { 54 | // TODO: Implement 55 | return "", nil 56 | } 57 | 58 | func IsOutdated(currentVersion, latestVersion string) bool { 59 | return latestVersion > currentVersion 60 | } 61 | 62 | func DownloadFFMPEGWindows(version, dir string) error { 63 | var err error 64 | 65 | filename := "ffmpeg-essentials_build.7z" 66 | archivePath := filepath.Join(dir, filename) 67 | 68 | // Download FFMPEG Archive 69 | url := fmt.Sprintf(FFMPEG_WIN_URL, version) 70 | Debugf("Downloading ffmpeg from: %s", url) 71 | err = DownloadFile(url, archivePath) 72 | if err != nil { 73 | return fmt.Errorf("Error downloading ffmpeg: %s", err) 74 | } 75 | 76 | Debug("Writing FFMPEG Version file...") 77 | err = WriteVersionFile(dir, version) 78 | if err != nil { 79 | return fmt.Errorf("Error writing ffmpeg version file: %s", err) 80 | } 81 | 82 | // Extract the FFMPEG Archive 83 | Debugf("Unzipping ffmpeg to %s...", dir) 84 | err = DecompressWFilter(archivePath, dir, fmt.Sprintf("ffmpeg-%s-essentials_build/", version), []string{"bin/ffmpeg.exe"}) 85 | if err != nil { 86 | return fmt.Errorf("Error unzipping ffmpeg: %s", err) 87 | } 88 | 89 | return nil 90 | } 91 | 92 | func DownloadFFMPEGMac(version, dir string) error { 93 | var err error 94 | 95 | filename := "ffmpeg.7z" 96 | archivePath := filepath.Join(dir, filename) 97 | 98 | // Download FFMPEG Archive 99 | url := fmt.Sprintf(FFMPEG_MAC_VERSION_INFO_URL, version) 100 | Debugf("Downloading ffmpeg from: %s", url) 101 | err = DownloadFile(url, archivePath) 102 | if err != nil { 103 | return fmt.Errorf("Error downloading ffmpeg: %s", err) 104 | } 105 | 106 | Debug("Writing FFMPEG Version file...") 107 | err = WriteVersionFile(dir, version) 108 | if err != nil { 109 | return fmt.Errorf("Error writing ffmpeg version file: %s", err) 110 | } 111 | 112 | // Extract the FFMPEG Archive 113 | Debugf("Unzipping ffmpeg to %s...", dir) 114 | err = DecompressWFilter(archivePath, dir, fmt.Sprintf("ffmpeg-%s/", version), []string{"ffmpeg"}) 115 | if err != nil { 116 | return fmt.Errorf("Error unzipping ffmpeg: %s", err) 117 | } 118 | 119 | return nil 120 | } 121 | 122 | func DownloadFFMPEGLinux(version, dir string) error { 123 | // TODO: At some point we should look into completing this 124 | return fmt.Errorf("It looks like you're running linux, this script does not support installing FFMPEG binaries for linux yet :(\nPlease install FFMPEG via your systems package manager and re-run the script") 125 | } 126 | 127 | // Function to get the latest version of FFMPEG for current platform 128 | func GetLatestFFMPEGVersion() (string, error) { 129 | switch runtime.GOOS { 130 | case "windows": 131 | return GetLatestWinFFMPEGVersion() 132 | case "darwin": 133 | return GetLatestMacFFMPEGVersion() 134 | case "linux": 135 | return GetLatestLinuxFFMPEGVersion() 136 | } 137 | 138 | return "", fmt.Errorf("Unsupported OS: %s", runtime.GOOS) 139 | } 140 | 141 | // Function to download the latest version of FFMPEG for current platform 142 | func DownloadFFMPEG(version, dir string) error { 143 | switch runtime.GOOS { 144 | case "windows": 145 | return DownloadFFMPEGWindows(version, dir) 146 | case "darwin": 147 | return DownloadFFMPEGMac(version, dir) 148 | case "linux": 149 | return DownloadFFMPEGLinux(version, dir) 150 | } 151 | 152 | return fmt.Errorf("Unsupported OS: %s", runtime.GOOS) 153 | } 154 | -------------------------------------------------------------------------------- /github.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/google/go-github/v43/github" 7 | ) 8 | 9 | var client = github.NewClient(nil) 10 | 11 | func GetRespository(owner, repo string) (*github.Repository, error) { 12 | rep, _, err := client.Repositories.Get(context.Background(), owner, repo) 13 | if err != nil { 14 | return nil, err 15 | } 16 | 17 | return rep, nil 18 | } 19 | 20 | func GetLatestRelease(owner, repo string) (*github.RepositoryRelease, error) { 21 | latestRelease, _, err := client.Repositories.GetLatestRelease(context.Background(), owner, repo) 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | return latestRelease, nil 27 | } 28 | 29 | func GetReleaseAssets(owner, repo string, id int64) ([]*github.ReleaseAsset, error) { 30 | opts := &github.ListOptions{PerPage: 100} 31 | assets, _, err := client.Repositories.ListReleaseAssets(context.Background(), owner, repo, id, opts) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | return assets, nil 37 | } 38 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/udemy-dl-go 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/google/go-github/v43 v43.0.0 7 | github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 8 | github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 9 | github.com/saracen/go7z v0.0.0-20191010121135-9c09b6bd7fda 10 | github.com/schollz/progressbar/v3 v3.8.6 11 | ) 12 | 13 | require ( 14 | github.com/fatih/color v1.13.0 // indirect 15 | github.com/google/go-querystring v1.1.0 // indirect 16 | github.com/mattn/go-colorable v0.1.9 // indirect 17 | github.com/mattn/go-isatty v0.0.14 // indirect 18 | github.com/mattn/go-runewidth v0.0.13 // indirect 19 | github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect 20 | github.com/rivo/uniseg v0.2.0 // indirect 21 | github.com/saracen/go7z-fixtures v0.0.0-20190623165746-aa6b8fba1d2f // indirect 22 | github.com/saracen/solidblock v0.0.0-20190426153529-45df20abab6f // indirect 23 | github.com/stretchr/testify v1.4.0 // indirect 24 | github.com/ulikunitz/xz v0.5.10 // indirect 25 | golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd // indirect 26 | golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8 // indirect 27 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect 28 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56 // indirect 29 | ) 30 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= 5 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= 6 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 7 | github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= 8 | github.com/google/go-github/v43 v43.0.0 h1:y+GL7LIsAIF2NZlJ46ZoC/D1W1ivZasT0lnWHMYPZ+U= 9 | github.com/google/go-github/v43 v43.0.0/go.mod h1:ZkTvvmCXBvsfPpTHXnH/d2hP9Y0cTbvN9kr5xqyXOIc= 10 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= 11 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 12 | github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 h1:qGQQKEcAR99REcMpsXCp3lJ03zYT1PkRd3kQGPn9GVg= 13 | github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= 14 | github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U= 15 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 16 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 17 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= 18 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 19 | github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= 20 | github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 21 | github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= 22 | github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= 23 | github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88= 24 | github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= 25 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 26 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 27 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 28 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 29 | github.com/saracen/go7z v0.0.0-20191010121135-9c09b6bd7fda h1:h+YpzUB/bGVJcLqW+d5GghcCmE/A25KbzjXvWJQi/+o= 30 | github.com/saracen/go7z v0.0.0-20191010121135-9c09b6bd7fda/go.mod h1:MSotTrCv1PwoR8QgU1JurEx+lNNbtr25I+m0zbLyAGw= 31 | github.com/saracen/go7z-fixtures v0.0.0-20190623165746-aa6b8fba1d2f h1:PF9WV5j/x6MT+x/sauUHd4objCvJbZb0wdxZkHSdd5A= 32 | github.com/saracen/go7z-fixtures v0.0.0-20190623165746-aa6b8fba1d2f/go.mod h1:6Ff0ADODZ6S3gYepgZ2w7OqFrTqtFcfwDUhmm8jsUhs= 33 | github.com/saracen/solidblock v0.0.0-20190426153529-45df20abab6f h1:1cJITU3JUI8qNS5T0BlXwANsVdyoJQHQ4hvOxbunPCw= 34 | github.com/saracen/solidblock v0.0.0-20190426153529-45df20abab6f/go.mod h1:LyBTue+RWeyIfN3ZJ4wVxvDuvlGJtDgCLgCb6HCPgps= 35 | github.com/schollz/progressbar/v3 v3.8.6 h1:QruMUdzZ1TbEP++S1m73OqRJk20ON11m6Wqv4EoGg8c= 36 | github.com/schollz/progressbar/v3 v3.8.6/go.mod h1:W5IEwbJecncFGBvuEh4A7HT1nZZ6WNIL2i3qbnI0WKY= 37 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 38 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 39 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 40 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 41 | github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8= 42 | github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= 43 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 44 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 45 | golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 46 | golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd h1:XcWmESyNjXJMLahc3mqVQJcgSTDxFxhETVlfk9uGc38= 47 | golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 48 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 49 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 50 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 51 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 52 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 53 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 54 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 55 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 56 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 57 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 58 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 59 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 60 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 61 | golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 62 | golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8 h1:OH54vjqzRWmbJ62fjuhxy7AxFFgoHN0/DPc/UrL8cAs= 63 | golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 64 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 65 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= 66 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 67 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 68 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 69 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 70 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56 h1:DFtSed2q3HtNuVazwVDZ4nSRS/JrZEig0gz2BY4VNrg= 71 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 72 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 73 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 74 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 75 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 76 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 77 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | 8 | "github.com/fatih/color" 9 | ) 10 | 11 | type ColorFunction func(format string, a ...interface{}) string 12 | 13 | type LogLevel struct { 14 | LevelName string 15 | Function ColorFunction 16 | } 17 | 18 | const ( 19 | SUCCESS = 0 20 | INFO = 1 21 | ERROR = 2 22 | DEBUG = 3 23 | WARNING = 4 24 | NOTICE = 5 25 | CRITICAL = 6 26 | ) 27 | 28 | func GetLogLevel(level int) LogLevel { 29 | switch level { 30 | case SUCCESS: 31 | return LogLevel{LevelName: "SUCCESS", Function: color.HiGreenString} 32 | case INFO: 33 | return LogLevel{LevelName: "INFO", Function: color.HiWhiteString} 34 | case ERROR: 35 | return LogLevel{LevelName: "ERROR", Function: color.HiRedString} 36 | case DEBUG: 37 | return LogLevel{LevelName: "DEBUG", Function: color.HiBlueString} 38 | case WARNING: 39 | return LogLevel{LevelName: "WARNING", Function: color.HiYellowString} 40 | case NOTICE: 41 | return LogLevel{LevelName: "NOTICE", Function: color.HiCyanString} 42 | case CRITICAL: 43 | return LogLevel{LevelName: "CRITICAL", Function: color.RedString} 44 | default: 45 | return LogLevel{LevelName: "INFO", Function: color.HiWhiteString} 46 | } 47 | } 48 | 49 | func Success(message string) { 50 | fmt.Println(fmt.Sprintf("%s %s ▶ %s", time.Now().Format("03:04:05"), "SUCCESS", color.HiGreenString(message))) 51 | } 52 | 53 | func Successf(format string, args ...interface{}) { 54 | fmt.Println(fmt.Sprintf("%s %s ▶ %s", time.Now().Format("03:04:05"), "SUCCESS", color.HiGreenString(fmt.Sprintf(format, args...)))) 55 | } 56 | 57 | func Info(message string) { 58 | fmt.Println(fmt.Sprintf("%s %s ▶ %s", time.Now().Format("03:04:05"), "INFO", color.HiWhiteString(message))) 59 | } 60 | 61 | func Infof(format string, args ...interface{}) { 62 | fmt.Println(fmt.Sprintf("%s %s ▶ %s", time.Now().Format("03:04:05"), "INFO", color.HiWhiteString(fmt.Sprintf(format, args...)))) 63 | } 64 | 65 | func Error(message string) { 66 | fmt.Println(fmt.Errorf("%s %s ▶ %s", time.Now().Format("03:04:05"), "ERROR", color.HiRedString(message))) 67 | } 68 | 69 | func Errorf(format string, args ...interface{}) { 70 | fmt.Println(fmt.Errorf("%s %s ▶ %s", time.Now().Format("03:04:05"), "ERROR", color.HiRedString(fmt.Sprintf(format, args...)))) 71 | } 72 | 73 | func Debug(message string) { 74 | fmt.Println(fmt.Sprintf("%s %s ▶ %s", time.Now().Format("03:04:05"), "DEBUG", color.HiBlueString(message))) 75 | } 76 | 77 | func Debugf(format string, args ...interface{}) { 78 | fmt.Println(fmt.Sprintf("%s %s ▶ %s", time.Now().Format("03:04:05"), "DEBUG", color.HiBlueString(fmt.Sprintf(format, args...)))) 79 | } 80 | 81 | func Warning(message string) { 82 | fmt.Println(fmt.Sprintf("%s %s ▶ %s", time.Now().Format("03:04:05"), "WARNING", color.HiYellowString(message))) 83 | } 84 | 85 | func Warningf(format string, args ...interface{}) { 86 | fmt.Println(fmt.Sprintf("%s %s ▶ %s", time.Now().Format("03:04:05"), "WARNING", color.HiYellowString(fmt.Sprintf(format, args...)))) 87 | } 88 | 89 | func Notice(message string) { 90 | fmt.Println(fmt.Sprintf("%s %s ▶ %s", time.Now().Format("03:04:05"), "NOTICE", color.HiCyanString(message))) 91 | } 92 | 93 | func Noticef(format string, args ...interface{}) { 94 | fmt.Println(fmt.Sprintf("%s %s ▶ %s", time.Now().Format("03:04:05"), "NOTICE", color.HiCyanString(fmt.Sprintf(format, args...)))) 95 | } 96 | 97 | func Critical(message string) { 98 | fmt.Println(fmt.Sprintf("%s %s ▶ %s", time.Now().Format("03:04:05"), "CRITICAL", color.RedString(message))) 99 | os.Exit(1) 100 | } 101 | 102 | func Criticalf(format string, args ...interface{}) { 103 | fmt.Println(fmt.Sprintf("%s %s ▶ %s", time.Now().Format("03:04:05"), "CRITICAL", color.RedString(fmt.Sprintf(format, args...)))) 104 | os.Exit(1) 105 | } 106 | 107 | func Log(level int, message string) { 108 | loglevel := GetLogLevel(level) 109 | fmt.Println(fmt.Sprintf("%s %s ▶ %s", time.Now().Format("03:04:05"), loglevel.LevelName, loglevel.Function(message))) 110 | } 111 | 112 | func Logf(level int, format string, args ...interface{}) { 113 | loglevel := GetLogLevel(level) 114 | fmt.Println(fmt.Sprintf("%s %s ▶ %s", time.Now().Format("03:04:05"), loglevel.LevelName, loglevel.Function(fmt.Sprintf(format, args...)))) 115 | } 116 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "os" 6 | ) 7 | 8 | var version string = "DEVELOPMENT" 9 | var debug bool = false 10 | 11 | func main() { 12 | // TODO: skip dependency check option 13 | // TODO: get course information 14 | // TODO: load from file argument 15 | // TODO: save to file argument 16 | // TODO: loading course information from file 17 | // TODO: save course information to file 18 | // TODO: get course content 19 | // TODO: process course content (this should be 'on the fly', so instead of pre-processing, just start downloading and fetch information for the lectures as we go) 20 | // TODO: info argument 21 | // TODO: mkv support 22 | 23 | if version == "DEVELOPMENT" { 24 | debug = true 25 | } 26 | 27 | versionPtr := flag.Bool("version", false, "Print the program version") 28 | // skipUpdatePtr := flag.Bool("skip-update", false, "Skip update check") 29 | bearerPtr := flag.String("bearer", "", "Bearer token for authentication") 30 | courseUrlPtr := flag.String("course", "", "Course URL") 31 | debugPtr := flag.Bool("debug", false, "Enable debug logging") 32 | flag.Parse() 33 | 34 | if *debugPtr { 35 | debug = true 36 | } 37 | 38 | if *versionPtr { 39 | Infof("Running version: %s", version) 40 | os.Exit(0) 41 | } 42 | 43 | if *bearerPtr == "" { 44 | Critical("A bearer token is required!") 45 | } 46 | 47 | if *courseUrlPtr == "" { 48 | Critical("A Course URL is required!") 49 | } 50 | 51 | ffmpegStatus, aria2Status, ytdlpStatus, shakaStatus, err := RunDependencyCheck() 52 | 53 | if err != nil { 54 | Criticalf("Dependency Check Error: %s", err) 55 | } 56 | 57 | // Print the status of all the checks 58 | if ffmpegStatus { 59 | Logf(SUCCESS, "FFMPEG: %t", ffmpegStatus) 60 | } else { 61 | Logf(ERROR, "FFMPEG: %t", ffmpegStatus) 62 | } 63 | 64 | if aria2Status { 65 | Logf(SUCCESS, "ARIA2: %t", aria2Status) 66 | } else { 67 | Logf(ERROR, "ARIA2: %t", aria2Status) 68 | } 69 | 70 | if ytdlpStatus { 71 | Logf(SUCCESS, "YTDLP: %t", ytdlpStatus) 72 | } else { 73 | Logf(ERROR, "YTDLP: %t", ytdlpStatus) 74 | } 75 | 76 | if shakaStatus { 77 | Logf(SUCCESS, "SHAKA: %t", shakaStatus) 78 | } else { 79 | Logf(ERROR, "SHAKA: %t", shakaStatus) 80 | } 81 | 82 | if !ffmpegStatus || !aria2Status || !ytdlpStatus || !shakaStatus { 83 | Critical("One or more dependencies are missing!") 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /scripts/build-linux.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | setlocal 4 | set GOOS=linux 5 | set GOARCH=amd64 6 | echo Building... 7 | go build -o ./dist/udemy-dl-go-linux-amd64 -v ./ 8 | 9 | endlocal 10 | -------------------------------------------------------------------------------- /scripts/build.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | go build -o ./dist/udemy-dl-go.exe ./ 4 | -------------------------------------------------------------------------------- /scripts/run.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | .\dist\udemy-dl-go.exe -bearer %1 -course %2 4 | -------------------------------------------------------------------------------- /updater.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | func FFMPEGCheck() (bool, error) { 9 | Info("Checking FFMPEG...") 10 | 11 | // check if ffmpeg is installed externally 12 | exists := CommandExists("ffmpeg") 13 | if exists { 14 | Success("FFMPEG appears to be installed already, probably via a package manager.") 15 | return true, nil 16 | } 17 | 18 | var err error 19 | 20 | // Ensure directory exists 21 | err = EnsureDirExist(FFMPEG_BIN_DIRECTORY) 22 | if err != nil { 23 | return false, fmt.Errorf("Error creating ffmpeg directory: %s", err) 24 | } 25 | 26 | // get the latest version of ffmpeg 27 | latestVersion, err := GetLatestFFMPEGVersion() 28 | if err != nil { 29 | return false, fmt.Errorf("Error getting latest ffmpeg version: %s", err) 30 | } 31 | 32 | // check for version file 33 | versionFileExists := VersionFileExists(FFMPEG_BIN_DIRECTORY) 34 | 35 | // existing install 36 | if versionFileExists { 37 | // read version from file 38 | currentVersion, err := ReadVersionFile(FFMPEG_BIN_DIRECTORY) 39 | if err != nil { 40 | return false, fmt.Errorf("Failed to read FFMPEG version file: %s", err) 41 | } 42 | 43 | // compare versions 44 | if IsOutdated(currentVersion, latestVersion) { 45 | // outdated 46 | Warningf("FFMPEG is outdated, current version: %s, latest version: %s", currentVersion, latestVersion) 47 | // remove old directory 48 | err = os.RemoveAll(FFMPEG_BIN_DIRECTORY) 49 | if err != nil { 50 | return false, err 51 | } 52 | // remake the directory 53 | err = EnsureDirExist(FFMPEG_BIN_DIRECTORY) 54 | if err != nil { 55 | return false, fmt.Errorf("Error creating ffmpeg directory: %s", err) 56 | } 57 | // download and extract 58 | err = DownloadFFMPEG(latestVersion, FFMPEG_BIN_DIRECTORY) 59 | if err != nil { 60 | return false, err 61 | } 62 | } else { 63 | // up to date 64 | Successf("FFMPEG is up to date, current version: %s, latest version: %s", currentVersion, latestVersion) 65 | } 66 | } else { 67 | // no existing install 68 | Warning("FFMPEG not found, downloading...") 69 | err = DownloadFFMPEG(latestVersion, FFMPEG_BIN_DIRECTORY) 70 | if err != nil { 71 | return false, err 72 | } 73 | } 74 | 75 | return true, nil 76 | } 77 | 78 | func GetLatestShakaPackagerVersion() (string, int64, error) { 79 | release, err := GetLatestRelease("shaka-project", "shaka-packager") 80 | if err != nil { 81 | // Errorf("Error getting latest shaka release: %s", err) 82 | return "", -1, err 83 | } 84 | 85 | return release.GetTagName(), release.GetID(), nil 86 | } 87 | 88 | func ShakaPackager() (bool, error) { 89 | // versionString, versionID, err := GetLatestShakaPackagerVersion() 90 | // if err != nil { 91 | // // Errorf("Error getting latest shaka release: %s", err) 92 | // return false, errors.New(fmt.Sprintf("Error getting latest shaka release: %s", err)) 93 | // } 94 | 95 | // Info("Latest Shaka Release: ", versionString) 96 | 97 | // assets, err := GetReleaseAssets("shaka-project", "shaka-packager", versionID) 98 | // if err != nil { 99 | // return false, errors.New(fmt.Sprintf("Error getting shaka release assets: %s", err)) 100 | // } 101 | 102 | // Info("Shaka Release Assets: ", assets) 103 | 104 | return true, nil 105 | } 106 | 107 | // ----------------------------- 108 | 109 | // Main dependency checking function, runs checks for each dependency depending on the current OS 110 | func RunDependencyCheck() (bool, bool, bool, bool, error) { 111 | Info("Starting dependency check") 112 | 113 | var err error 114 | var ffmpegStatus bool = false 115 | var aria2Status bool = false 116 | var ytdlpStatus bool = false 117 | var shakaStatus bool = false 118 | 119 | // ensure the main bin directory exists 120 | // ensure bin dir exists 121 | err = EnsureDirExist("bin") 122 | if err != nil { 123 | return ffmpegStatus, aria2Status, ytdlpStatus, shakaStatus, fmt.Errorf("Error creating ffmpeg directory: %s", err) 124 | } 125 | 126 | // tries to make bin directory whether it exists or not 127 | err = EnsureDirExist("bin") 128 | if err != nil { 129 | return ffmpegStatus, aria2Status, ytdlpStatus, shakaStatus, fmt.Errorf("Error creating bin directory: %s", err) 130 | } 131 | 132 | // TODO: add other dependencies (aria2, yt-dlp, shaka-packager) 133 | ffmpegStatus, err = FFMPEGCheck() 134 | 135 | if err != nil { 136 | return ffmpegStatus, aria2Status, ytdlpStatus, shakaStatus, err 137 | } 138 | 139 | return ffmpegStatus, aria2Status, ytdlpStatus, shakaStatus, nil 140 | } 141 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "io/ioutil" 7 | "net/http" 8 | "os" 9 | "os/exec" 10 | "path" 11 | "path/filepath" 12 | "strings" 13 | 14 | "github.com/k0kubun/go-ansi" 15 | "github.com/saracen/go7z" 16 | 17 | "github.com/schollz/progressbar/v3" 18 | ) 19 | 20 | var httpClient = &http.Client{} 21 | 22 | func GetUrl(url string) (*http.Response, error) { 23 | return httpClient.Get(url) 24 | } 25 | 26 | func GetText(url string) (string, error) { 27 | resp, err := GetUrl(url) 28 | if err != nil { 29 | return "", err 30 | } 31 | defer resp.Body.Close() 32 | 33 | data, err := io.ReadAll(resp.Body) 34 | if err != nil { 35 | return "", err 36 | } 37 | 38 | return string(data), nil 39 | } 40 | 41 | func GetBytes(url string) ([]byte, error) { 42 | resp, err := GetUrl(url) 43 | if err != nil { 44 | return nil, err 45 | } 46 | defer resp.Body.Close() 47 | 48 | data, err := io.ReadAll(resp.Body) 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | return data, nil 54 | } 55 | 56 | func EnsureDirExist(path string) error { 57 | err := os.MkdirAll(path, os.ModePerm) 58 | if err != nil { 59 | return err 60 | } 61 | 62 | return nil 63 | } 64 | 65 | func FileExists(path string) bool { 66 | _, err := os.Stat(path) 67 | 68 | if err == nil { 69 | return true 70 | } 71 | 72 | if os.IsNotExist(err) { 73 | return false 74 | } 75 | 76 | return false 77 | } 78 | 79 | func VersionFileExists(dir string) bool { 80 | fpath := filepath.Join(dir, ".version") 81 | return FileExists(fpath) 82 | 83 | } 84 | 85 | func GetVersionFile(dir string) (string, error) { 86 | // read a file 87 | file, err := os.Open(filepath.Join(dir, ".version")) 88 | if err != nil { 89 | return "", err 90 | } 91 | defer file.Close() 92 | 93 | // read file into a string 94 | data, err := ioutil.ReadAll(file) 95 | if err != nil { 96 | return "", err 97 | } 98 | 99 | return string(data), nil 100 | } 101 | 102 | func DownloadFile(url, filepath string) (err error) { 103 | fname := path.Base(filepath) 104 | 105 | // Get the data 106 | resp, err := http.Get(url) 107 | if err != nil { 108 | return err 109 | } 110 | defer resp.Body.Close() 111 | 112 | // Check server response 113 | if resp.StatusCode != http.StatusOK { 114 | return fmt.Errorf("bad status: %s", resp.Status) 115 | } 116 | 117 | // Create the file 118 | out, err := os.Create(filepath) 119 | if err != nil { 120 | return err 121 | } 122 | defer out.Close() 123 | 124 | bar := progressbar.NewOptions(int(resp.ContentLength), 125 | progressbar.OptionSetWriter(ansi.NewAnsiStdout()), 126 | progressbar.OptionEnableColorCodes(true), 127 | progressbar.OptionShowBytes(true), 128 | progressbar.OptionSetWidth(15), 129 | progressbar.OptionSetDescription(fmt.Sprintf("[cyan][reset] Downloading %s...", fname)), 130 | progressbar.OptionSetTheme(progressbar.Theme{ 131 | Saucer: "[green]=[reset]", 132 | SaucerHead: "[green]>[reset]", 133 | SaucerPadding: " ", 134 | BarStart: "[", 135 | BarEnd: "]", 136 | })) 137 | 138 | // Writer the body to file 139 | _, err = io.Copy(io.MultiWriter(out, bar), resp.Body) 140 | if err != nil { 141 | return err 142 | } 143 | println("") 144 | 145 | return nil 146 | } 147 | 148 | func DecompressWFilter(source, dest, remoteArchiveName string, filters []string) error { 149 | sz, err := go7z.OpenReader(source) 150 | if err != nil { 151 | return err 152 | } 153 | defer sz.Close() 154 | 155 | for { 156 | hdr, err := sz.Next() 157 | if err == io.EOF { 158 | break // End of archive 159 | } 160 | if err != nil { 161 | return err 162 | } 163 | 164 | // remove root directory from path 165 | hdr.Name = strings.TrimPrefix(hdr.Name, remoteArchiveName) 166 | 167 | for _, filter := range filters { 168 | if strings.HasPrefix(hdr.Name, filter) { 169 | Debugf("%s matches filter %s", hdr.Name, filter) 170 | fname := filepath.Base(hdr.Name) 171 | p := filepath.Join(dest, fname) 172 | 173 | // If empty stream (no contents) and isn't specifically an empty file... 174 | // then it's a directory. 175 | if hdr.IsEmptyStream && !hdr.IsEmptyFile { 176 | if err := os.MkdirAll(p, os.ModePerm); err != nil { 177 | return err 178 | } 179 | continue 180 | } 181 | 182 | // Create file 183 | f, err := os.Create(p) 184 | if err != nil { 185 | return err 186 | } 187 | defer f.Close() 188 | 189 | if _, err := io.Copy(f, sz); err != nil { 190 | return err 191 | } 192 | } 193 | } 194 | } 195 | 196 | return nil 197 | } 198 | 199 | func WriteVersionFile(dir string, version string) error { 200 | header := "// This file is automatically generated DO NOT EDIT THIS FILE\n" 201 | // make file path 202 | fpath := filepath.Join(dir, ".version") 203 | err := ioutil.WriteFile(fpath, []byte(header+version), 0644) 204 | if err != nil { 205 | return err 206 | } 207 | 208 | return nil 209 | } 210 | 211 | func ReadVersionFile(dir string) (string, error) { 212 | // make file path 213 | fpath := filepath.Join(dir, ".version") 214 | data, err := ioutil.ReadFile(fpath) 215 | if err != nil { 216 | return "", err 217 | } 218 | 219 | lines := strings.Split(string(data), "\n") 220 | return lines[1], nil 221 | } 222 | 223 | func CommandExists(cmd string) bool { 224 | _, err := exec.LookPath(cmd) 225 | return err == nil 226 | } 227 | --------------------------------------------------------------------------------