Skip to content

Commit

Permalink
Kinda initial commit.
Browse files Browse the repository at this point in the history
- A good structured go project
- Used some cool dependencies
- Go project, go purr
  • Loading branch information
AYehia0 committed Jan 7, 2023
1 parent 859091f commit 589de06
Show file tree
Hide file tree
Showing 13 changed files with 358 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@
# Output of the go coverage tool, specifically when used with LiteIDE
*.out

sound/
# Dependency directories (remove the comment below to include it)
# vendor/
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# SC

A go project to download sound tracks from soundcloud!
24 changes: 24 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# TODO

- Before Starting:

- [x] Learn the basics of go lang.
- Learn working with streams and files
- Learn how to send requests
- Learn how to structure the code in go
- Learn some OOP
- Learn Async and multi-threading

- Get the client ID ?
- Make a GET request to : `https://soundcloud.com`
- Find the last asset : `https://a-v2.sndcdn.com/assets/*`
- Make a get request to that url and extract the `client_id`

- Validate client_id ?
- Make a get request to `/me` and check the status response.

- Get auth_token ?
- Find your OAuth token by visiting SoundCloud after logging in and watching any of the browsers requests to the SoundCloud API, the token will be under the `Authorization` header of any of these requests

- soundcloud uses OAuth so the Authorization header should be as `OAuth {auth_token}`
- Download a track ?
39 changes: 39 additions & 0 deletions cmd/soundcloud-dl/parse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package soundclouddl

import (
"fmt"
"log"

"github.com/AYehia0/soundcloud-dl/internal"
"github.com/spf13/cobra"
)

var rootCmd = &cobra.Command{
Use: "sc <url>",
Short: "Sc is a simple CLI application to download soundcloud tracks",
Long: `A blazingly fast go program to download tracks from soundcloud
using just the URL, with some cool features and beautiful UI.
`,
Args: cobra.ArbitraryArgs,
Run: func(cmd *cobra.Command, args []string) {
// get the URL
// TODO: check cobra docs for a cleaner way to do this
if len(args) < 1 {
if err := cmd.Usage(); err != nil {
log.Fatal(err)
}
return
}
// run the core app
internal.Sc(args)
},
}

func Execute() {
// initialize the arg parser variables
InitConfigVars()

if err := rootCmd.Execute(); err != nil {
fmt.Printf("Something went wrong : %s\n", err)
}
}
8 changes: 8 additions & 0 deletions cmd/soundcloud-dl/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package soundclouddl

var Search bool

// define flags and handle configuration
func InitConfigVars() {
rootCmd.PersistentFlags().BoolVarP(&Search, "search", "s", false, "Does the track exist ?")
}
15 changes: 15 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module github.com/AYehia0/soundcloud-dl

go 1.19

require (
github.com/PuerkitoBio/goquery v1.8.0
github.com/spf13/cobra v1.6.1
)

require (
github.com/andybalholm/cascadia v1.3.1 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8 // indirect
)
21 changes: 21 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U=
github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI=
github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8 h1:/6y1LfuqNuQdHAm0jjtPtgRcxIxjVZgm5OTu8/QhZvk=
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
44 changes: 44 additions & 0 deletions internal/core.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package internal

import (
"bytes"
"fmt"
"log"
"net/http"

"github.com/AYehia0/soundcloud-dl/pkg/client"
"github.com/AYehia0/soundcloud-dl/pkg/soundcloud"
"github.com/PuerkitoBio/goquery"
)

func Sc(args []string) {

url := args[0]

statusCode, body, err := client.Get(url)

if err != nil {
log.Fatalf("An Error : %s happended while requesting : %s", err, url)
}
if statusCode != http.StatusOK {
fmt.Println("URL doesn't exist : status not 200.")
return
}

// conver the bytes array into something we can read, as goquery doesn't accept strings
doc, err := goquery.NewDocumentFromReader(bytes.NewReader(body))

// check if the url is valid
if !soundcloud.IsValidUrl(url) {
fmt.Printf("The Url : %s doesn't return a valid track are you sure the track is publicly accessed ?", url)
return
}

soundData := soundcloud.IsValidTrack(doc)

if soundData == nil {
fmt.Printf("The Url : %s doesn't return a valid track are you sure the track is publicly accessed ?", url)
return
}
fmt.Println("ALL OK :D")
}
8 changes: 8 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package main

import soundclouddl "github.com/AYehia0/soundcloud-dl/cmd/soundcloud-dl"

func main() {
// call the cmd Run command to execute the whole program
soundclouddl.Execute()
}
63 changes: 63 additions & 0 deletions pkg/client/http.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
Modify the http package to allow easy calls
*/
package client

import (
"io/ioutil"
"net/http"
"net/url"
"time"
)

type HTTP struct {
client *http.Client
}

// timeout the http request to a level and sets a proxy is passed
func New(timeout time.Duration, proxy string) (*HTTP, error) {
httpConn := &HTTP{
client: http.DefaultClient,
}

if timeout > 0 {
httpConn.client.Timeout = timeout
}

if proxy != "" {
// parse the url
proxy, err := url.Parse(proxy)

if err != nil {
return nil, err
}

t := http.DefaultTransport.(*http.Transport)
t.Proxy = func(*http.Request) (*url.URL, error) {
return proxy, nil
}
httpConn.client.Transport = t
}

return httpConn, nil
}

// make a GET request and return some info about the request
func Get(url string) (int, []byte, error) {
resp, err := http.Get(url)

if err != nil {
return -1, nil, err
}

// read the response body
bodyBytes, err := ioutil.ReadAll(resp.Body)

defer resp.Body.Close()

if err != nil {
return resp.StatusCode, nil, err
}

return resp.StatusCode, bodyBytes, nil
}
63 changes: 63 additions & 0 deletions pkg/soundcloud/page.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package soundcloud

import (
"encoding/json"
"regexp"
"strings"

"github.com/PuerkitoBio/goquery"
)

// convert the data map[string]any to well defined struct
// by marshalling then unmarshalling, which is probably isn't the best thing todo.
func formatData(data map[string]any) (*SoundData, error) {
sound := &SoundData{}
jsonBody, _ := json.Marshal(data)
if err := json.Unmarshal(jsonBody, &sound); err != nil {
// do error check
return nil, err
}
// adding the transcodings
// go goes crazy mode lol
transcodings := data["media"].(map[string]interface{})["transcodings"].([]any)

for _, val := range transcodings {
x := val.(map[string]any)
encoding := Transcode{
ApiUrl: x["url"].(string),
Quality: x["quality"].(string),
Format: x["format"].(map[string]interface{})["mime_type"].(string),
}
sound.Transcodings = append(sound.Transcodings, encoding)
}

return sound, nil
}

// extract some meta data under : window.__sc_hydration
func GetSoundMetaData(doc *goquery.Document) *SoundData {

page := doc.First().Text()

// TODO: You can do it in one shot, without splitting the text, but meh
re := regexp.MustCompile(`(?m)^window.__sc_hydration = (.*?);\s*$`)
matches := re.FindAllStringSubmatch(page, 1)

if len(matches) < 1 {
return nil
}

var result []map[string]interface{}
data := strings.Split(matches[0][1], "\n")[0]

json.Unmarshal([]byte(data), &result)

// extracting the data
// userData := result[6]["data"]
soundData := result[7]["data"].(map[string]any)

// TODO: return the error here
dataSound, _ := formatData(soundData)

return dataSound
}
22 changes: 22 additions & 0 deletions pkg/soundcloud/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package soundcloud

type Transcode struct {
ApiUrl string
Quality string
Format string
}

type SoundData struct {
Id int64 `json:"id"`
Title string `json:"title"`
CreatedAt string `json:"created_at"`
Duration int64 `json:"duration"`
Kind string `json:"kind"`
PermalinkUrl string `json:"permalink_url"`
UserId int64 `json:"user_id"`
Transcodings []Transcode
TrackAuth string `json:"track_authorization"`
LikesCount int `json:"likes_count"`
Downloadable bool `json:"downloadable"`
Description string `json:"description,omitempty"`
}
47 changes: 47 additions & 0 deletions pkg/soundcloud/url.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
Check if the URL passed is a valid URL by matching a regex.
*/
package soundcloud

import (
"log"
"regexp"

"github.com/PuerkitoBio/goquery"
)

// check if the url is a soundcloud url
func IsValidUrl(url string) bool {

/*
^ - start of string
(?:https?://)? - an optional http:// or https://
(?:[^/\s]+\.)* - zero or more repetitions of :
[^/.\s]+ - one or more chars other than /, . and whitespace
\. - a dot
google\.com - an escaped keyword
(?:/[^/\s]+)* - zero or more repetitions of a / and then one or more chars other than / and whitespace chars
/? - an optional /
$ - end of string
*/
pattern := `^(?:https?://)?(?:[^/.\s]+\.)*soundcloud\.com(?:/[^/\s]+)*/?$`
matched, err := regexp.MatchString(pattern, url)
if err != nil {
log.Fatalf("Something went wrong while parsing the URL : %s", err)
}

if matched {
return true
}
return false
}

// check if the track exists and open to public
// by checking the html for something unique like : likes,
// the url must be a IsValidUrl first
func IsValidTrack(doc *goquery.Document) *SoundData {

sound := GetSoundMetaData(doc)

return sound
}

0 comments on commit 589de06

Please sign in to comment.