├── .gitignore ├── internal ├── downloader │ ├── downloader.go │ └── steamworkshop_downloader.go └── extractor │ └── extractor.go ├── go.mod ├── README.md ├── go.sum └── cmd └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | bin 3 | -------------------------------------------------------------------------------- /internal/downloader/downloader.go: -------------------------------------------------------------------------------- 1 | package downloader 2 | 3 | type Downloader interface { 4 | Download(string) string 5 | } 6 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ArtyomArtamonov/wallpaper-engine-downloader 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/BurntSushi/toml v1.0.0 7 | golang.org/x/tools v0.0.0-20190624190245-7f2218787638 8 | ) 9 | 10 | require ( 11 | github.com/PuerkitoBio/goquery v1.8.0 // indirect 12 | github.com/andybalholm/cascadia v1.3.1 // indirect 13 | golang.org/x/net v0.0.0-20210916014120-12bc252f5db8 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /internal/extractor/extractor.go: -------------------------------------------------------------------------------- 1 | package extractor 2 | 3 | import ( 4 | "archive/zip" 5 | "fmt" 6 | "io" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | ) 11 | 12 | func Extract(destination string, archiveName string) { 13 | archive, err := zip.OpenReader(archiveName) 14 | if err != nil { 15 | panic(err) 16 | } 17 | defer archive.Close() 18 | 19 | for _, f := range archive.File { 20 | filePath := filepath.Join(destination, f.Name) 21 | fmt.Println("unzipping file ", filePath) 22 | 23 | if !strings.HasPrefix(filePath, filepath.Clean(destination)+string(os.PathSeparator)) { 24 | fmt.Println("invalid file path") 25 | return 26 | } 27 | if f.FileInfo().IsDir() { 28 | fmt.Println("creating directory...") 29 | os.MkdirAll(filePath, os.ModePerm) 30 | continue 31 | } 32 | 33 | if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil { 34 | panic(err) 35 | } 36 | 37 | dstFile, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) 38 | if err != nil { 39 | panic(err) 40 | } 41 | defer dstFile.Close() 42 | 43 | fileInArchive, err := f.Open() 44 | if err != nil { 45 | panic(err) 46 | } 47 | defer fileInArchive.Close() 48 | 49 | if _, err := io.Copy(dstFile, fileInArchive); err != nil { 50 | panic(err) 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wallpaper-engine-downloader 2 | 3 | If you can't just add wallpapers from steamworkshop, you have to download them via steamworhshop downloaders, unzip and put in projects folder. 4 | Tired of doing this every time? Just use wallpaper-engine-downloader 5 | 6 | ## Usage 7 | 8 | Download ready-to-go windows .exe file 9 | 10 | ### Prepare config.toml file 11 | 12 | Locate wallpaper engine folder. 13 | It contains directory 'projects', which contains 'myprojects'. 14 | Copy path to 'myprojects' folder 15 | 16 | In the directory containing executable, create file named config.toml. 17 | Right click, Edit 18 | 19 | ```toml 20 | MyprojectPath = "C:\\Escaped\\Path\\To\\myproject\\folder" 21 | ``` 22 | 23 | ### Using precompiled .exe file 24 | 25 | Open console and proceed into directory containing executable 26 | 27 | -config config-file-name.toml (should be in same directory) 28 | 29 | -wallpaper link-to-wallpaper in steamworkshop 30 | 31 | ```bash 32 | ./wallpaper-engine-downloader.exe -config config.toml -wallpaper "https://steamcommunity.com/sharedfiles/filedetails/?id=818603284&searchtext=" 33 | ``` 34 | 35 | UPD: Since v0.8 you can place your config.toml file next to .exe file. The program will read its contents. 36 | Since v0.8 you can specify wallpaper workshop link in terminal after executing the binary. It will read from stdin in loop, if you want to download more than one wallpaper. 37 | 38 | ## Credentials 39 | 40 | This program uses http://steamworkshop.download/ to download items from steamworkshop. Future versions could support steam API to download directly from steam, but for now, I leave it as is. 41 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v1.0.0 h1:dtDWrepsVPfW9H/4y7dDgFc2MBUSeJhlaDtK13CxFlU= 2 | github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 3 | github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U= 4 | github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI= 5 | github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= 6 | github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= 7 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 8 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 9 | golang.org/x/net v0.0.0-20210916014120-12bc252f5db8 h1:/6y1LfuqNuQdHAm0jjtPtgRcxIxjVZgm5OTu8/QhZvk= 10 | golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 11 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 12 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 13 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 14 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 15 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 16 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 17 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 18 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 19 | golang.org/x/tools v0.0.0-20190624190245-7f2218787638 h1:uIfBkD8gLczr4XDgYpt/qJYds2YJwZRNw4zs7wSnNhk= 20 | golang.org/x/tools v0.0.0-20190624190245-7f2218787638/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 21 | -------------------------------------------------------------------------------- /internal/downloader/steamworkshop_downloader.go: -------------------------------------------------------------------------------- 1 | package downloader 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "io/ioutil" 7 | "log" 8 | "net/http" 9 | "net/url" 10 | "os" 11 | "path" 12 | "path/filepath" 13 | "strings" 14 | 15 | "github.com/PuerkitoBio/goquery" 16 | ) 17 | 18 | const WALLPAPER_ENGINE_STEAM_APP_ID = "431960" 19 | 20 | type SteamWorkshopDownloader struct { 21 | link string 22 | } 23 | 24 | func NewSteamWorkshopDownloader() *SteamWorkshopDownloader { 25 | return &SteamWorkshopDownloader{ 26 | link: "http://steamworkshop.download/online/steamonline.php", 27 | } 28 | } 29 | 30 | func (s SteamWorkshopDownloader) Download(wallpaper string) string { 31 | execPath, err := os.Executable() 32 | if err != nil { 33 | log.Fatal(err.Error()) 34 | } 35 | 36 | id, err := getIdFromWorkshopLink(wallpaper) 37 | if err != nil { 38 | log.Fatal(err.Error()) 39 | } 40 | 41 | data := url.Values{ 42 | "app": {WALLPAPER_ENGINE_STEAM_APP_ID}, 43 | "item": {id}, 44 | } 45 | resp, err := http.PostForm(s.link, data) 46 | if err != nil { 47 | log.Fatalf(err.Error()) 48 | } 49 | defer resp.Body.Close() 50 | 51 | downloadLink := getDownloadLink(resp) 52 | archive, err := http.Get(downloadLink) 53 | if err != nil { 54 | log.Fatal(err.Error()) 55 | } 56 | defer archive.Body.Close() 57 | 58 | var result []byte 59 | archive.Body.Read(result) 60 | 61 | downloadFolder, err := ioutil.TempDir(filepath.Dir(execPath), "temp") 62 | if err != nil { 63 | log.Fatal(err.Error()) 64 | } 65 | 66 | out, _ := os.Create(path.Join(downloadFolder, id + ".zip")) 67 | defer out.Close() 68 | io.Copy(out, archive.Body) 69 | 70 | return out.Name() 71 | } 72 | 73 | func getDownloadLink(resp *http.Response) string { 74 | doc, err := goquery.NewDocumentFromReader(resp.Body) 75 | if err != nil { 76 | log.Fatal(err.Error()) 77 | } 78 | 79 | link, exists := doc.Find("a").Attr("href") 80 | if !exists { 81 | log.Fatal("Could not found download link") 82 | } 83 | 84 | return link 85 | } 86 | 87 | func getIdFromWorkshopLink(link string) (string, error) { 88 | values, err := url.ParseQuery(link) 89 | if err != nil { 90 | return "", errors.New("could not fetch id from url") 91 | } 92 | 93 | for key, val := range values { 94 | if strings.Contains(key, "id") { 95 | return val[0], nil 96 | } 97 | } 98 | 99 | return "", errors.New("string not found") 100 | } 101 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "flag" 6 | "fmt" 7 | "log" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | 12 | "github.com/ArtyomArtamonov/wallpaper-engine-downloader/internal/downloader" 13 | "github.com/ArtyomArtamonov/wallpaper-engine-downloader/internal/extractor" 14 | "github.com/BurntSushi/toml" 15 | ) 16 | 17 | var workshopLink = flag.String("wallpaper", "", "Link to wallpaper in steam workshop") 18 | var wallpaperEngineProjectPath = flag.String("myproject", "", "where wallpapers will be unziped to") 19 | var configPath = flag.String("config", "", "Used to provide downloader with data such as wallpaper engine project folder etc.") 20 | 21 | func main() { 22 | input := prepareInput() 23 | 24 | for { 25 | if input.wallpaperWorkshopLink == "" { 26 | input.wallpaperWorkshopLink = askForWallpaperLinkFromInput() 27 | } 28 | 29 | dwnl := downloader.NewSteamWorkshopDownloader() 30 | 31 | wallpaper := dwnl.Download(input.wallpaperWorkshopLink) 32 | 33 | extractor.Extract(input.myproject, wallpaper) 34 | 35 | // Delete temporary directory and all files in it 36 | downloadDir := filepath.Join(filepath.Dir(wallpaper)) 37 | log.Printf("Removing temp folder %s", downloadDir) 38 | err := os.RemoveAll(downloadDir) 39 | if err != nil { 40 | log.Fatal(err.Error()) 41 | } 42 | 43 | input.wallpaperWorkshopLink = "" 44 | } 45 | } 46 | 47 | type Input struct { 48 | myproject string 49 | wallpaperWorkshopLink string 50 | } 51 | 52 | type config struct { 53 | MyprojectPath string 54 | } 55 | 56 | func prepareInput() *Input { 57 | input := &Input{} 58 | 59 | flag.Parse() 60 | 61 | input.wallpaperWorkshopLink = *workshopLink 62 | input.myproject = *wallpaperEngineProjectPath 63 | 64 | var file []byte 65 | var err error 66 | 67 | if *configPath == "" { 68 | file, err = getDefaultConfig() 69 | if err != nil { 70 | log.Fatal(err.Error()) 71 | } 72 | 73 | } else { 74 | file, err = os.ReadFile(*configPath) 75 | if err != nil { 76 | log.Fatal(err.Error()) 77 | } 78 | } 79 | 80 | var config config 81 | err = toml.Unmarshal(file, &config) 82 | if err != nil { 83 | log.Fatal(err.Error()) 84 | } 85 | 86 | input.myproject = config.MyprojectPath 87 | input.wallpaperWorkshopLink = *workshopLink 88 | 89 | if input.myproject == "" { 90 | log.Fatal("ERROR: Could not find myproject path." + 91 | "Please specify it in config as 'myproject' or pass as flag -myproject") 92 | } 93 | 94 | return input 95 | } 96 | 97 | func askForWallpaperLinkFromInput() string { 98 | reader := bufio.NewReader(os.Stdin) 99 | fmt.Println("Enter wallpaper workshop link (or n/N to exit): ") 100 | text, _ := reader.ReadString('\n') 101 | text = strings.ReplaceAll(text, "\r\n", "") 102 | 103 | if strings.ToLower(text) == "n" { 104 | os.Exit(0) 105 | } 106 | 107 | return text 108 | } 109 | 110 | func getDefaultConfig() ([]byte, error) { 111 | execPath, err := os.Executable() 112 | if err != nil { 113 | log.Fatal(err.Error()) 114 | } 115 | 116 | thisPath := filepath.Join(filepath.Dir(execPath)) 117 | defaultConfigPath := filepath.Join(thisPath, "config.toml") 118 | 119 | return os.ReadFile(defaultConfigPath) 120 | } 121 | --------------------------------------------------------------------------------