├── go.mod ├── cmd_linux.go ├── cmd_windows.go ├── Readme.txt ├── Dockerfile └── main.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Orygin/SoundCloudParallelDL 2 | 3 | go 1.24.3 4 | -------------------------------------------------------------------------------- /cmd_linux.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os/exec" 5 | "syscall" 6 | ) 7 | 8 | const ( 9 | cmdBinName = "yt-dlp" 10 | workingDir = "/data/" 11 | ) 12 | 13 | func aggrementCmd(cmd *exec.Cmd) { 14 | cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} 15 | } 16 | -------------------------------------------------------------------------------- /cmd_windows.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os/exec" 5 | "syscall" 6 | ) 7 | 8 | const ( 9 | cmdBinName = "./youtube-dl.exe" 10 | workingDir = "." 11 | ) 12 | 13 | func aggrementCmd(cmd *exec.Cmd) { 14 | cmd.SysProcAttr = &syscall.SysProcAttr{CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP} 15 | } 16 | -------------------------------------------------------------------------------- /Readme.txt: -------------------------------------------------------------------------------- 1 | SoundCloud downloader. Based on the script written by /u/redditgoogle @ https://www.reddit.com/r/Music/comments/4597e6/soundcloud_could_be_forced_to_close_after_44m/czw8q9q 2 | You have to download youtube-dl to use this. Linux & mac users can use the script above or have to remove the ".exe" in the youtube-dl exec path and build. 3 | Will save files in the current working directory. Options are maxparallel which defines how many artists to download in parallel, and silent which disable youtube-dl stdout. -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Start by building the application. 2 | FROM golang:1.24.3-alpine as build 3 | 4 | WORKDIR /go/src/app 5 | COPY . . 6 | 7 | RUN ls 8 | RUN cat go.mod && CGO_ENABLED=0 go build -o /go/bin/app 9 | 10 | # Now copy it into our base image. 11 | FROM alpine 12 | 13 | COPY --from=build /go/bin/app / 14 | 15 | RUN apk add curl python3 ffmpeg 16 | RUN curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -o /bin/yt-dlp 17 | RUN chmod a+rx /bin/yt-dlp # Make executable 18 | 19 | CMD ["/app", "--cookiefile", "$COOKIES"] -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Based on the script written by /u/redditgoogle @ https://www.reddit.com/r/Music/comments/4597e6/soundcloud_could_be_forced_to_close_after_44m/czw8q9q 2 | package main 3 | 4 | import ( 5 | "bufio" 6 | "context" 7 | "flag" 8 | "log" 9 | "math/rand/v2" 10 | "os" 11 | "os/exec" 12 | "strconv" 13 | "strings" 14 | "syscall" 15 | "time" 16 | ) 17 | 18 | var maxParallel = flag.Int("maxparallel", 1, "Defines the maximum parrallel artists to download") 19 | var silent = flag.Bool("silent", false, "Defines if the youtube-dl output is to be printed") 20 | var cookieFile = flag.String("cookiefile", "", "Defines the cookie file") 21 | 22 | func main() { 23 | flag.Parse() 24 | var artists []string 25 | list, err := os.Open(workingDir + "artistlist.txt") 26 | if err != nil { 27 | log.Printf("Could not open artist list file : %v", err) 28 | return 29 | } 30 | 31 | scanner := bufio.NewScanner(list) 32 | 33 | var total = 0 34 | for scanner.Scan() { 35 | total++ 36 | artist := scanner.Text() 37 | artists = append(artists, artist) 38 | } 39 | list.Close() 40 | log.Println("downloading " + strconv.Itoa(total)) 41 | 42 | rand.Shuffle(len(artists), func(i, j int) { 43 | artists[i], artists[j] = artists[j], artists[i] 44 | }) 45 | 46 | for { 47 | for _, artist := range artists { 48 | fetch(artist) 49 | } 50 | time.Sleep(time.Hour * 24) 51 | } 52 | } 53 | 54 | func fetch(artist string) { 55 | log.Println("Running for " + artist) 56 | args := []string{ 57 | "soundcloud.com/" + artist + "/tracks", 58 | "-o", workingDir + artist + "/%(artist)s-%(title)s.%(ext)s", 59 | //"-x", "--audio-format", "flac", "--audio-quality", "8", 60 | /*"--add-metadata", "--write-description", */ "-w", "--no-progress", 61 | "--sleep-requests", "6", "-k", 62 | //"--extractor-args", "soundcloud:formats=*_aac", 63 | } 64 | if *cookieFile != "" { 65 | args = append(args, "--cookies", *cookieFile) 66 | } 67 | log.Printf("args: %s", strings.Join(args, " ")) 68 | 69 | ctx, cancel := context.WithCancel(context.Background()) 70 | cancelled := false 71 | 72 | cmd := exec.CommandContext(ctx, cmdBinName, args...) 73 | if !*silent { 74 | cmd.Stdout = NewLogWriter(artist, cancel) 75 | } 76 | cmd.Stderr = os.Stderr 77 | cmd.Cancel = func() error { 78 | cancelled = true 79 | proc, err := os.FindProcess(-cmd.Process.Pid) 80 | if err != nil { 81 | log.Printf("Could not find process: %v", err) 82 | return err 83 | } 84 | return proc.Signal(syscall.SIGTERM) 85 | } 86 | aggrementCmd(cmd) 87 | 88 | if err := cmd.Run(); err != nil { 89 | log.Printf("could not run ytdlp: %s", err.Error()) 90 | if !cancelled { 91 | os.Exit(1) 92 | } 93 | } 94 | log.Println("Finished " + artist) 95 | } 96 | 97 | type LogWriter struct { 98 | artist string 99 | progress, max int 100 | stop context.CancelFunc 101 | } 102 | 103 | func NewLogWriter(artist string, stop context.CancelFunc) *LogWriter { 104 | return &LogWriter{ 105 | artist: artist, 106 | stop: stop, 107 | } 108 | } 109 | 110 | func (lw LogWriter) Write(p []byte) (n int, err error) { 111 | str := string(p) 112 | log.Println(lw.artist + ": " + string(p)) 113 | if strings.Contains(str, "has already been downloaded") { 114 | lw.stop() 115 | } 116 | // I tried matching the youtube-dl progress but it's harder than I thought to get it right 117 | /*match, _ := regexp.Match(".*?(\\[download\\])( )(Downloading)( )(video)( )(\\d+)( )(of)( )(\\d+).*?", p) 118 | if match { 119 | log.Println(lw.artist + ": " + string(p)) 120 | }*/ 121 | return len(p), nil 122 | } 123 | --------------------------------------------------------------------------------