├── go.mod ├── LICENSE ├── class.go ├── README.md ├── downloader.go ├── session.go ├── go.sum ├── .github └── workflows │ └── release.yml └── cmd └── skillshare-dl └── main.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/iochen/skillshare-dl 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/kennygrant/sanitize v1.2.4 7 | github.com/remeh/sizedwaitgroup v1.0.0 8 | github.com/sirupsen/logrus v1.7.0 9 | golang.org/x/net v0.0.0-20210415231046-e915ea6b2b7d // indirect 10 | ) 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Richard Chen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /class.go: -------------------------------------------------------------------------------- 1 | package skillshare_dl 2 | 3 | type ClassInfo struct { 4 | ID int `json:"id"` 5 | Sku int `json:"sku"` 6 | Title string `json:"title"` 7 | ProjectTitle string `json:"project_title"` 8 | Embedded struct { 9 | Teacher struct { 10 | Username int `json:"username"` 11 | FullName string `json:"full_name"` 12 | VanityUsername interface{} `json:"vanity_username"` 13 | } `json:"teacher"` 14 | Units struct { 15 | Embedded struct { 16 | Units []struct { 17 | Embedded struct { 18 | Sessions struct { 19 | Embedded struct { 20 | Sessions []Session `json:"sessions"` 21 | } `json:"_embedded"` 22 | } `json:"sessions"` 23 | } `json:"_embedded"` 24 | } `json:"units"` 25 | } `json:"_embedded"` 26 | } `json:"units"` 27 | } `json:"_embedded"` 28 | } 29 | 30 | func (ci *ClassInfo) AllSessions() []*Session { 31 | var sessions []*Session 32 | for i := range ci.Embedded.Units.Embedded.Units { 33 | for j := range ci.Embedded.Units.Embedded.Units[i].Embedded.Sessions.Embedded.Sessions { 34 | sessions = append(sessions, &ci.Embedded.Units.Embedded.Units[i].Embedded.Sessions.Embedded.Sessions[j]) 35 | } 36 | } 37 | return sessions 38 | } 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SkillShare Downloader 2 | This project aims to help skillshare premium users easier to access their 3 | lessons when network is unstable or not available. 4 | 5 | ! DO **NOT** use this project for piracy ! 6 | 7 | ## Feature 8 | - 5 threads by default 9 | - ... 10 | 11 | ## Install 12 | ### Linux AMD64 13 | ```bash 14 | $ sudo wget https://github.com/iochen/skillshare-dl/releases/latest/download/skillshare-dl_amd64_linux -O /usr/bin/skillshare-dl 15 | $ sudo chmod +x /usr/bin/skillshare-dl 16 | ``` 17 | ### Other Platforms 18 | 1. Download from https://github.com/iochen/skillshare-dl/releases/latest 19 | 2. rename to `skillshare-dl` 20 | 3. give execute permission to it 21 | 4. add or put it to your path 22 | 23 | ## Quick start 24 | 1. Login to your skillshare premium account on browser, press **F12** and type 25 | ```javascript 26 | document.cookie 27 | ``` 28 | in **Console** 29 | 30 | 2. Copy and save the output to a file such as `cookie.txt` 31 | 32 | 3. run(or build it yourself) **skillshare-dl**, with command like below 33 | ```bash 34 | $ skillshare-dl -cookie cookie.txt -id 970659408 35 | # or 36 | $ skillshare-dl -cookie cookie.txt -id 970659408 -id 652554100 -id ... 37 | ``` 38 | 39 | ## Thanks 40 | [kallqvist/skillshare-downloader](https://github.com/kallqvist/skillshare-downloader) 41 | -------------------------------------------------------------------------------- /downloader.go: -------------------------------------------------------------------------------- 1 | package skillshare_dl 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "strings" 8 | ) 9 | 10 | type Downloader struct { 11 | cookie string 12 | } 13 | 14 | func parseCookie(cookie string) string { 15 | cookie = strings.TrimSpace(cookie) 16 | cookie = strings.Trim(cookie, `"`) 17 | return cookie 18 | } 19 | 20 | func NewDownloader(cookie string) *Downloader { 21 | return &Downloader{ 22 | cookie: parseCookie(cookie), 23 | } 24 | } 25 | 26 | func (dl *Downloader) Cookie(cookie string) { 27 | dl.cookie = parseCookie(cookie) 28 | } 29 | 30 | func (dl *Downloader) GetInfo(id int) (*ClassInfo, error) { 31 | req, err := http.NewRequest("GET", fmt.Sprintf("https://api.skillshare.com/classes/%d", id), nil) 32 | if err != nil { 33 | return &ClassInfo{}, err 34 | } 35 | req.Header.Add("Accept", "application/vnd.skillshare.class+json;,version=0.8") 36 | req.Header.Add("User-Agent", "Skillshare/5.3.0; Android 9.0.1") 37 | req.Header.Add("Referer", "https://www.skillshare.com/") 38 | req.Header.Add("Cookie", dl.cookie) 39 | 40 | resp, err := http.DefaultClient.Do(req) 41 | if err != nil { 42 | return &ClassInfo{}, err 43 | } 44 | defer resp.Body.Close() 45 | 46 | if resp.StatusCode != http.StatusOK { 47 | return &ClassInfo{}, fmt.Errorf("cannot get video info, response with code %d", resp.StatusCode) 48 | } 49 | 50 | info := &ClassInfo{} 51 | 52 | err = json.NewDecoder(resp.Body).Decode(info) 53 | return info, err 54 | } 55 | -------------------------------------------------------------------------------- /session.go: -------------------------------------------------------------------------------- 1 | package skillshare_dl 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "strings" 8 | ) 9 | 10 | type Session struct { 11 | ID int `json:"id"` 12 | ParentClassSku int `json:"parent_class_sku"` 13 | Title string `json:"title"` 14 | VideoHashedID string `json:"video_hashed_id"` 15 | } 16 | 17 | type videoSourceInfo struct { 18 | Sources []*Video `json:"sources"` 19 | } 20 | 21 | type Video struct { 22 | Codecs string `json:"codecs,omitempty"` 23 | ExtXVersion string `json:"ext_x_version,omitempty"` 24 | Src string `json:"src"` 25 | Type string `json:"type,omitempty"` 26 | Profiles string `json:"profiles,omitempty"` 27 | AvgBitrate int `json:"avg_bitrate,omitempty"` 28 | Codec string `json:"codec,omitempty"` 29 | Container string `json:"container,omitempty"` 30 | Duration int `json:"duration,omitempty"` 31 | Height int `json:"height,omitempty"` 32 | Width int `json:"width,omitempty"` 33 | } 34 | 35 | func (s *Session) Video(account, pk string) ([]*Video, error) { 36 | vid := strings.Split(s.VideoHashedID, ":")[1] 37 | url := fmt.Sprintf("https://edge.api.brightcove.com/playback/v1/accounts/%s/videos/%s", account, vid) 38 | req, err := http.NewRequest("GET", url, nil) 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | req.Header.Add("Accept", fmt.Sprintf("application/json;pk=%s", pk)) 44 | req.Header.Add("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101 Firefox/52.0") 45 | req.Header.Add("Origin", "https://www.skillshare.com") 46 | 47 | resp, err := http.DefaultClient.Do(req) 48 | if err != nil { 49 | return nil, err 50 | } 51 | defer resp.Body.Close() 52 | 53 | vsi := &videoSourceInfo{} 54 | err = json.NewDecoder(resp.Body).Decode(vsi) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | return vsi.Sources, nil 60 | } 61 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/kennygrant/sanitize v1.2.4 h1:gN25/otpP5vAsO2djbMhF/LQX6R7+O1TB4yv8NzpJ3o= 4 | github.com/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak= 5 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 7 | github.com/remeh/sizedwaitgroup v1.0.0 h1:VNGGFwNo/R5+MJBf6yrsr110p0m4/OX4S3DCy7Kyl5E= 8 | github.com/remeh/sizedwaitgroup v1.0.0/go.mod h1:3j2R4OIe/SeS6YDhICBy22RWjJC5eNCJ1V+9+NVNYlo= 9 | github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM= 10 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 11 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= 12 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 13 | golang.org/x/net v0.0.0-20210415231046-e915ea6b2b7d h1:BgJvlyh+UqCUaPlscHJ+PN8GcpfrFdr7NHjd1JL0+Gs= 14 | golang.org/x/net v0.0.0-20210415231046-e915ea6b2b7d/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= 15 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 16 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 17 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44 h1:Bli41pIlzTzf3KEY06n+xnzK/BESIg2ze4Pgfh/aI8c= 18 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 19 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 20 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 21 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 22 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: 5 | - 'v*' 6 | 7 | jobs: 8 | Build-and-Release: 9 | name: build and release 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: checkout 13 | uses: actions/checkout@v1 14 | 15 | - name: set up Go 1.14 16 | uses: actions/setup-go@v1 17 | with: 18 | go-version: 1.14 19 | id: go 20 | 21 | - name: set variables 22 | run: | 23 | mkdir build 24 | echo "VERSION=${GITHUB_REF##*/}" >> $GITHUB_ENV 25 | shell: bash 26 | env: 27 | GITHUB_REF: ${{ github.ref }} 28 | 29 | - name: build 30 | run: | 31 | LDFLAGS="-s -w -X main.Version=${{ env.VERSION }}" 32 | CMDPATH=./cmd/skillshare-dl/ 33 | GOOS=linux GOARCH=amd64 go build -ldflags="${LDFLAGS}" -gcflags=-trimpath=${GOPATH} -asmflags=-trimpath=${GOPATH} -v -o ./build/skillshare-dl_amd64_linux ${CMDPATH} 34 | GOOS=linux GOARCH=arm64 go build -ldflags="${LDFLAGS}" -gcflags=-trimpath=${GOPATH} -asmflags=-trimpath=${GOPATH} -v -o ./build/skillshare-dl_arm64_linux ${CMDPATH} 35 | GOOS=linux GOARCH=386 go build -ldflags="${LDFLAGS}" -gcflags=-trimpath=${GOPATH} -asmflags=-trimpath=${GOPATH} -v -o ./build/skillshare-dl_386_linux ${CMDPATH} 36 | GOOS=linux GOARCH=arm go build -ldflags="${LDFLAGS}" -gcflags=-trimpath=${GOPATH} -asmflags=-trimpath=${GOPATH} -v -o ./build/skillshare-dl_arm_linux ${CMDPATH} 37 | GOOS=windows GOARCH=amd64 go build -ldflags="${LDFLAGS}" -gcflags=-trimpath=${GOPATH} -asmflags=-trimpath=${GOPATH} -v -o ./build/skillshare-dl_amd64_windows.exe ${CMDPATH} 38 | GOOS=windows GOARCH=386 go build -ldflags="${LDFLAGS}" -gcflags=-trimpath=${GOPATH} -asmflags=-trimpath=${GOPATH} -v -o ./build/skillshare-dl_386_windows.exe ${CMDPATH} 39 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="${LDFLAGS} -extldflags \"-static\"" -gcflags=-trimpath=${GOPATH} -asmflags=-trimpath=${GOPATH} -v -o ./build/skillshare-dl_amd64_linux_static ${CMDPATH} 40 | - name: compress 41 | uses: actions-github/upx@master 42 | with: 43 | dir: './build' 44 | upx_args: '-9' 45 | 46 | - name: release 47 | uses: softprops/action-gh-release@v1 48 | if: startsWith(github.ref, 'refs/tags/') 49 | with: 50 | files: | 51 | ./build/skillshare-dl_amd64_linux 52 | ./build/skillshare-dl_arm64_linux 53 | ./build/skillshare-dl_386_linux 54 | ./build/skillshare-dl_arm_linux 55 | ./build/skillshare-dl_amd64_windows.exe 56 | ./build/skillshare-dl_386_windows.exe 57 | ./build/skillshare-dl_amd64_linux_static 58 | ./LICENSE 59 | env: 60 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /cmd/skillshare-dl/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "net/http" 10 | "os" 11 | "strconv" 12 | "strings" 13 | 14 | "github.com/kennygrant/sanitize" 15 | "github.com/remeh/sizedwaitgroup" 16 | "github.com/sirupsen/logrus" 17 | 18 | ssdl "github.com/iochen/skillshare-dl" 19 | ) 20 | 21 | type idList []int 22 | 23 | func (l *idList) String() string { 24 | return "video id list" 25 | } 26 | 27 | func (l *idList) Set(value string) error { 28 | v, err := strconv.Atoi(value) 29 | if err != nil { 30 | return err 31 | } 32 | *l = append(*l, v) 33 | return nil 34 | } 35 | 36 | func main() { 37 | var list idList 38 | cf := flag.String("cookie", "cookie.txt", "the file stored cookie") 39 | flag.Var(&list, "id", "video id") 40 | flag.Parse() 41 | 42 | bytes, err := ioutil.ReadFile(*cf) 43 | if err != nil { 44 | logrus.Fatal(err) 45 | } 46 | 47 | fmt.Println("Video ID List:", list) 48 | 49 | for i := range list { 50 | fmt.Printf("=========== Downloading %d ===========\n", i) 51 | logrus.Error(Download(string(bytes), list[i])) 52 | fmt.Printf("=========== Video %d has been downloaded! ===========\n", i) 53 | } 54 | } 55 | 56 | func Download(cookie string, id int) error { 57 | dl := ssdl.NewDownloader(cookie) 58 | classInfo, err := dl.GetInfo(id) 59 | if err != nil { 60 | return err 61 | } 62 | 63 | sessions := classInfo.AllSessions() 64 | 65 | dir := sanitize.BaseName(classInfo.Title) 66 | if err := os.MkdirAll(dir, os.ModePerm); err != nil { 67 | return err 68 | } 69 | 70 | wg := sizedwaitgroup.New(5) 71 | for i := range sessions { 72 | wg.Add() 73 | session := *sessions[i] 74 | go func(i int, session ssdl.Session) { 75 | defer wg.Done() 76 | video, err := session.Video("3695997568001", "BCpkADawqM2OOcM6njnM7hf9EaK6lIFlqiXB0iWjqGWUQjU7R8965xUvIQNqdQbnDTLz0IAO7E6Ir2rIbXJtFdzrGtitoee0n1XXRliD-RH9A-svuvNW9qgo3Bh34HEZjXjG4Nml4iyz3KqF") 77 | if err != nil { 78 | logrus.Error(err) 79 | return 80 | } 81 | 82 | title := sanitize.BaseName(strings.ReplaceAll(session.Title, "/", `-`)) 83 | path := fmt.Sprintf("%s/%d.%s.mp4", dir, i, title) 84 | if _, err := os.Stat(path); !os.IsNotExist(err) { 85 | fmt.Println("EXISTED:", path) 86 | return 87 | } 88 | 89 | err = FetchVideo(path, video) 90 | if err != nil { 91 | logrus.Error(err) 92 | return 93 | } else { 94 | fmt.Println("FINISHED:", path) 95 | } 96 | }(i, session) 97 | } 98 | wg.Wait() 99 | 100 | return nil 101 | } 102 | 103 | func FetchVideo(path string, video []*ssdl.Video) error { 104 | var url string 105 | for i := range video { 106 | if video[i].Container == "MP4" { 107 | url = video[i].Src 108 | } 109 | } 110 | if url == "" { 111 | return errors.New("cannot get available video src") 112 | } 113 | 114 | resp, err := http.Get(url) 115 | if err != nil { 116 | return err 117 | } 118 | 119 | file, err := os.Create(path) 120 | if err != nil { 121 | return err 122 | } 123 | defer file.Close() 124 | 125 | _, err = io.Copy(file, resp.Body) 126 | return err 127 | } 128 | --------------------------------------------------------------------------------